Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,37 @@ When `assignmentStrategy.type` is `Single`, you can set `assignmentStrategy.inst

If `instantMatch` is non-empty, `acurast deploy` skips the market pricing check for that project, since matching is explicit.

```json
{
"projects": {
"my-instant-match-project": {
"projectName": "my-instant-match-project",
"fileUrl": "examples/canary-test.js",
"network": "canary",
"onlyAttestedDevices": true,
"assignmentStrategy": {
"type": "Single",
"instantMatch": [
{
"processor": "5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL",
"maxAllowedStartDelayInMs": 10000
}
]
},
"execution": { "type": "onetime", "maxExecutionTimeInMs": 60000 },
"maxAllowedStartDelayInMs": 10000,
"usageLimit": {
"maxMemory": 0,
"maxNetworkRequests": 0,
"maxStorage": 0
},
"numberOfReplicas": 1,
"maxCostPerExecution": 1000000000
}
}
}
```

### Shell Runtime

The Shell runtime runs your deployment as a native binary inside a Linux distro image on the processor (PRoot-isolated). This unlocks shell scripts, native tooling, and any language you can ship as a Linux binary.
Expand Down
20 changes: 19 additions & 1 deletion src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,13 @@ import type { AcurastSigner } from '@acurast/sdk/chain'
import {
getSigningMode,
getLoggedInAddress,
getExpiredAuth,
touchAuth,
} from '../util/authStore.js'
import { signingNoticeLine } from '../util/signingNotice.js'
import {
signingNoticeLine,
expiredSessionMessage,
} from '../util/signingNotice.js'
import { startSignServer } from '../util/cliServer.js'
import { RemoteSigner } from '../acurast/remoteSigner.js'
import { buildDeploySummary } from '../acurast/deploySummary.js'
Expand Down Expand Up @@ -501,6 +505,20 @@ export async function executeDeployFlow(
const signingMode = getSigningMode()
filelogger.info(`Signing mode: ${signingMode}`)

// An aged-out login silently resolves back to `local`, so without a mnemonic
// the env-var check below would blame a missing ACURAST_MNEMONIC. Say what
// actually happened instead.
if (signingMode === 'local' && !process.env.ACURAST_MNEMONIC) {
const expired = getExpiredAuth()
if (expired) {
filelogger.warn(
`Login expired (${expired.scope}), last logged in at ${expired.record.loggedInAt}`
)
log(expiredSessionMessage(expired))
return
}
}

try {
validateDeployEnvVars({ requireMnemonic: signingMode === 'local' })
} catch (e: any) {
Expand Down
30 changes: 26 additions & 4 deletions src/util/authStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,43 @@ const isExpired = (record: AuthRecord): boolean => {
return Date.now() - at > SESSION_MAX_AGE_MS
}

const read = (scope: AuthScope): AuthRecord | null => {
/** The stored record as-is, without the expiry check. */
const parse = (scope: AuthScope): AuthRecord | null => {
const raw = store(scope).getItem(AUTH_KEY)
if (!raw) return null
try {
const record = JSON.parse(raw) as AuthRecord
if (isExpired(record)) return null
return record
return JSON.parse(raw) as AuthRecord
} catch {
return null
}
}

const read = (scope: AuthScope): AuthRecord | null => {
const record = parse(scope)
if (!record || isExpired(record)) return null
return record
}

export const getGlobalAuth = (): AuthRecord | null => read('global')
export const getProjectAuth = (): AuthRecord | null => read('project')

/**
* A stored login that exists but has aged out, if any (project pin first, to
* match `getActiveAuth`). Lets callers say "your session expired" instead of
* the misleading "ACURAST_MNEMONIC is not defined" they would otherwise hit
* once an expired session drops the signing mode back to `local`.
*/
export const getExpiredAuth = (): {
scope: AuthScope
record: AuthRecord
} | null => {
for (const scope of ['project', 'global'] as const) {
const record = parse(scope)
if (record && isExpired(record)) return { scope, record }
}
return null
}

/** The account that will actually be used: a project pin wins over the global login. */
export const getActiveAuth = (): AuthRecord | null => getProjectAuth() ?? getGlobalAuth()

Expand Down
34 changes: 33 additions & 1 deletion src/util/signingNotice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SigningMode } from './authStore.js'
import type { AuthRecord, AuthScope, SigningMode } from './authStore.js'

/**
* One-line, user-facing notice shown at deploy time so it is always clear how
Expand All @@ -9,3 +9,35 @@ export const signingNoticeLine = (mode: SigningMode, address: string): string =>
mode === 'local'
? `Signing with local mnemonic (${address}) — private key is read from your environment.`
: `Signing with your browser wallet (${address}).`

const daysAgo = (iso: string): number | null => {
const at = Date.parse(iso)
if (Number.isNaN(at)) return null
return Math.floor((Date.now() - at) / (24 * 60 * 60 * 1000))
}

/**
* Shown when the only thing standing between the user and a deploy is an aged-out
* login. Without this they get `"ACURAST_MNEMONIC" is not defined in the
* environment.` — technically true (the expired session dropped the signing mode
* back to `local`) but it points at the wrong fix.
*/
export const expiredSessionMessage = (expired: {
scope: AuthScope
record: AuthRecord
}): string => {
const { scope, record } = expired
const days = daysAgo(record.loggedInAt)
const since = days === null ? '' : ` ${days} days ago`
const where =
scope === 'project'
? 'This project is pinned to an account whose session expired'
: 'Your Acurast session expired'

return [
`${where}${since} (${record.address}).`,
'',
'Run `acurast login` to sign in with your browser wallet again,',
'or set ACURAST_MNEMONIC to sign locally instead.',
].join('\n')
}
40 changes: 40 additions & 0 deletions test/authStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const {
isLoggedIn,
getSigningMode,
getAuthSource,
getExpiredAuth,
} = await import('../src/util/authStore.js')

const record = (address: string) => ({
Expand Down Expand Up @@ -114,6 +115,45 @@ describe('authStore', () => {
})
})

describe('getExpiredAuth', () => {
const stale = (address: string) => ({
...record(address),
loggedInAt: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(),
})

test('surfaces an expired global session that getGlobalAuth hides', () => {
setAuth(stale('5Old'), 'global')
expect(getGlobalAuth()).toBeNull()
expect(getExpiredAuth()).toMatchObject({
scope: 'global',
record: { address: '5Old' },
})
})

test('prefers the project pin, matching getActiveAuth', () => {
setAuth(stale('5OldGlobal'), 'global')
setAuth(stale('5OldProject'), 'project')
expect(getExpiredAuth()).toMatchObject({
scope: 'project',
record: { address: '5OldProject' },
})
})

test('null when the session is still valid', () => {
setAuth(record('5Fresh'), 'global')
expect(getExpiredAuth()).toBeNull()
})

test('null when nothing is stored', () => {
expect(getExpiredAuth()).toBeNull()
})

test('null on malformed JSON', () => {
storeFor(ACURAST_GLOBAL_BASE_PATH).set('auth', '{ not json')
expect(getExpiredAuth()).toBeNull()
})
})

describe('getSigningMode resolution order', () => {
test('1. explicit ACURAST_SIGNING_MODE wins over everything', () => {
setAuth(record('5Global'), 'global')
Expand Down
46 changes: 45 additions & 1 deletion test/signingNotice.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { signingNoticeLine } from '../src/util/signingNotice.js'
import {
signingNoticeLine,
expiredSessionMessage,
} from '../src/util/signingNotice.js'

describe('signingNoticeLine', () => {
test('local mnemonic notice names the address and warns about key on disk', () => {
Expand All @@ -14,3 +17,44 @@ describe('signingNoticeLine', () => {
expect(line.toLowerCase()).toContain('browser wallet')
})
})

describe('expiredSessionMessage', () => {
const staleRecord = (address: string, days: number) => ({
address,
signatureType: 'sr25519',
network: 'canary' as const,
loggedInAt: new Date(
Date.now() - days * 24 * 60 * 60 * 1000
).toISOString(),
})

test('names the account, the age, and both ways out', () => {
const message = expiredSessionMessage({
scope: 'global',
record: staleRecord('5Alice', 21),
})
expect(message).toContain('5Alice')
expect(message).toContain('21 days ago')
expect(message).toContain('acurast login')
expect(message).toContain('ACURAST_MNEMONIC')
})

test('says the project is pinned when the expired login is a project pin', () => {
const message = expiredSessionMessage({
scope: 'project',
record: staleRecord('5Bob', 30),
})
expect(message.toLowerCase()).toContain('project')
expect(message.toLowerCase()).toContain('pinned')
})

test('omits the age when loggedInAt is unparseable', () => {
const message = expiredSessionMessage({
scope: 'global',
record: { ...staleRecord('5Carol', 21), loggedInAt: 'not-a-date' },
})
expect(message).toContain('5Carol')
expect(message).not.toContain('NaN')
expect(message).toContain('acurast login')
})
})
Loading