@astrale-os/cli 0.8.0-alpha.0 → 0.8.1-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/astrale.js +62968 -41240
- package/dist/public/connect-core.js +55030 -0
- package/dist/public/keys/index.js +45631 -0
- package/dist/public/paths/index.js +43990 -0
- package/dist/types/admin/instance/model.d.ts +40 -0
- package/dist/types/connect-core.d.ts +22 -0
- package/dist/types/connection/auth.d.ts +33 -0
- package/dist/types/errors.d.ts +23 -0
- package/dist/types/identity/registry.d.ts +25 -0
- package/dist/types/keys/algorithm.d.ts +6 -0
- package/dist/types/keys/credential.d.ts +21 -0
- package/dist/types/keys/index.d.ts +4 -0
- package/dist/types/keys/pair.d.ts +34 -0
- package/dist/types/lib/admin-target.d.ts +74 -0
- package/dist/types/lib/ca-fetch.d.ts +2 -0
- package/dist/types/lib/config.d.ts +71 -0
- package/dist/types/lib/format.d.ts +1 -0
- package/dist/types/lib/idp-session.d.ts +33 -0
- package/dist/types/lib/idp.d.ts +397 -0
- package/dist/types/lib/instance-target.d.ts +36 -0
- package/dist/types/lib/instance.d.ts +186 -0
- package/dist/types/lib/log.d.ts +30 -0
- package/dist/types/lib/login-flow.d.ts +49 -0
- package/dist/types/lib/meta.d.ts +36 -0
- package/dist/types/lib/output.d.ts +83 -0
- package/dist/types/lib/table.d.ts +18 -0
- package/dist/types/lib/validation.d.ts +11 -0
- package/dist/types/paths/index.d.ts +2 -0
- package/dist/types/state/exchange-credentials.d.ts +25 -0
- package/dist/types/state/files.d.ts +9 -0
- package/dist/types/state/identities.d.ts +42 -0
- package/dist/types/state/index.d.ts +8 -0
- package/dist/types/state/paths.d.ts +32 -0
- package/package.json +30 -8
- package/src/admin/.spec/api.d.ts +3 -0
- package/src/admin/.spec/architecture.md +15 -0
- package/src/admin/.spec/icon.svg +3 -0
- package/src/admin/.spec/layout.ts +6 -0
- package/src/admin/catalog/.spec/api.d.ts +65 -0
- package/src/admin/catalog/.spec/architecture.md +6 -0
- package/src/admin/catalog/.spec/icon.svg +3 -0
- package/src/admin/catalog/.spec/layout.ts +6 -0
- package/src/admin/catalog/__tests__/client.test.ts +171 -0
- package/src/admin/catalog/client.ts +214 -0
- package/src/admin/catalog/index.ts +13 -0
- package/src/admin/catalog/model.ts +42 -0
- package/src/admin/catalog/tsconfig.json +8 -0
- package/src/admin/graph/.spec/api.d.ts +28 -0
- package/src/admin/graph/.spec/architecture.md +5 -0
- package/src/admin/graph/.spec/icon.svg +3 -0
- package/src/admin/graph/.spec/layout.ts +3 -0
- package/src/admin/graph/index.ts +6 -0
- package/src/admin/graph/nodes.ts +56 -0
- package/src/admin/index.ts +2 -0
- package/src/admin/instance/.spec/api.d.ts +66 -0
- package/src/admin/instance/.spec/architecture.md +10 -0
- package/src/admin/instance/.spec/icon.svg +3 -0
- package/src/admin/instance/.spec/layout.ts +6 -0
- package/src/admin/instance/__tests__/client.test.ts +289 -0
- package/src/admin/instance/client.ts +298 -0
- package/src/admin/instance/index.ts +16 -0
- package/src/admin/instance/model.ts +65 -0
- package/src/admin/instance/tsconfig.json +8 -0
- package/src/commands/__tests__/admin-instance.test.ts +3 -36
- package/src/commands/__tests__/call.test.ts +21 -68
- package/src/commands/__tests__/domain-install-owned.test.ts +26 -27
- package/src/commands/__tests__/domain-list.test.ts +2 -6
- package/src/commands/__tests__/logs.test.ts +50 -98
- package/src/commands/__tests__/ls.test.ts +21 -17
- package/src/commands/__tests__/read-commands.test.ts +171 -101
- package/src/commands/__tests__/view.test.ts +100 -51
- package/src/commands/admin/status.ts +3 -3
- package/src/commands/admin/use.ts +29 -6
- package/src/commands/auth/login.ts +1 -1
- package/src/commands/auth/logout.ts +4 -2
- package/src/commands/auth/status.ts +2 -2
- package/src/commands/auth/token.ts +2 -2
- package/src/commands/browser.ts +1 -1
- package/src/commands/call.ts +55 -91
- package/src/commands/describe.ts +52 -147
- package/src/commands/domain/install.ts +81 -50
- package/src/commands/domain/list.ts +4 -8
- package/src/commands/domain/publish.ts +9 -31
- package/src/commands/get.ts +32 -65
- package/src/commands/identity/__tests__/register.test.ts +93 -0
- package/src/commands/identity/create.ts +2 -2
- package/src/commands/identity/delete.ts +2 -2
- package/src/commands/identity/export.ts +7 -33
- package/src/commands/identity/import.ts +21 -70
- package/src/commands/identity/list.ts +2 -2
- package/src/commands/identity/register.ts +148 -117
- package/src/commands/identity/sync.ts +2 -2
- package/src/commands/identity/unsync.ts +2 -2
- package/src/commands/identity/use.ts +2 -2
- package/src/commands/identity/whoami.ts +2 -2
- package/src/commands/idp/add.ts +1 -1
- package/src/commands/idp/list.ts +1 -1
- package/src/commands/idp/refresh.ts +1 -1
- package/src/commands/idp/remove.ts +2 -2
- package/src/commands/idp/show.ts +1 -1
- package/src/commands/instance/active.ts +2 -2
- package/src/commands/instance/bookmark.ts +8 -2
- package/src/commands/instance/create.ts +1 -1
- package/src/commands/instance/delete.ts +4 -10
- package/src/commands/instance/forget.ts +1 -1
- package/src/commands/instance/list.ts +3 -3
- package/src/commands/instance/status.ts +5 -7
- package/src/commands/instance/use.ts +3 -3
- package/src/commands/logs.ts +152 -279
- package/src/commands/ls.ts +78 -204
- package/src/commands/mutate.ts +70 -139
- package/src/commands/query.ts +100 -278
- package/src/commands/session/analyze.ts +1 -1
- package/src/commands/session/list.ts +1 -1
- package/src/commands/setup.ts +1 -1
- package/src/commands/status.ts +1 -1
- package/src/commands/studio.ts +1 -1
- package/src/commands/token.ts +26 -21
- package/src/commands/update.ts +1 -1
- package/src/commands/use.ts +2 -3
- package/src/commands/view-serve.ts +1 -1
- package/src/commands/view.ts +67 -109
- package/src/connect-core.test.ts +38 -0
- package/src/connect-core.ts +42 -0
- package/src/connection/.spec/api.d.ts +111 -0
- package/src/connection/.spec/architecture.md +41 -0
- package/src/connection/.spec/capabilities/connection.ts +13 -0
- package/src/connection/.spec/examples/call.ts +7 -0
- package/src/connection/.spec/flows/command.ts +20 -0
- package/src/connection/.spec/flows/session.ts +61 -0
- package/src/connection/.spec/laws/connection.ts +181 -0
- package/src/connection/.spec/layout.ts +19 -0
- package/src/{kernel → connection}/__tests__/auth.test.ts +26 -2
- package/src/connection/__tests__/ca-fetch.test.ts +53 -0
- package/src/connection/__tests__/call.test.ts +17 -0
- package/src/connection/__tests__/credential.test.ts +109 -0
- package/src/connection/__tests__/errors.test.ts +96 -0
- package/src/connection/__tests__/exchange.test.ts +224 -0
- package/src/connection/__tests__/fixtures/localhost-cert.pem +19 -0
- package/src/connection/__tests__/fixtures/localhost-key.base64 +1 -0
- package/src/connection/__tests__/managed.test.ts +39 -0
- package/src/connection/__tests__/self.test.ts +51 -0
- package/src/connection/__tests__/session.test.ts +140 -0
- package/src/connection/__tests__/target.test.ts +93 -0
- package/src/{kernel → connection}/auth.ts +8 -8
- package/src/connection/call.ts +7 -0
- package/src/{kernel/run.ts → connection/command.ts} +22 -13
- package/src/connection/credential.ts +114 -0
- package/src/{kernel → connection}/errors.ts +32 -9
- package/src/connection/exchange.ts +274 -0
- package/src/connection/index.ts +8 -0
- package/src/connection/self.ts +120 -0
- package/src/connection/session.ts +176 -0
- package/src/connection/target.ts +111 -0
- package/src/connection/tsconfig.json +6 -0
- package/src/graph/.spec/api.d.ts +34 -0
- package/src/graph/.spec/laws/documents.ts +24 -0
- package/src/graph/.spec/laws/projection.ts +13 -0
- package/src/graph/.spec/layout.ts +6 -0
- package/src/graph/__tests__/mutation.test.ts +43 -0
- package/src/graph/__tests__/projection.test.ts +22 -0
- package/src/graph/__tests__/query.test.ts +154 -0
- package/src/graph/index.ts +3 -0
- package/src/graph/mutation.ts +18 -0
- package/src/graph/projection.ts +21 -0
- package/src/graph/query.ts +108 -0
- package/src/graph/tsconfig.json +6 -0
- package/src/identity/.history/ard/bootstrap-root-transfer.md +57 -0
- package/src/identity/.history/ard/domain-mediated-registration.md +36 -0
- package/src/identity/.spec/api.d.ts +124 -0
- package/src/identity/.spec/architecture.md +33 -0
- package/src/identity/.spec/capabilities/identity.ts +7 -0
- package/src/identity/.spec/flows/bootstrap-root.ts +79 -0
- package/src/identity/.spec/flows/domain-mediated-registration.ts +51 -0
- package/src/identity/.spec/flows/import.ts +29 -0
- package/src/identity/.spec/laws/identity.ts +56 -0
- package/src/identity/.spec/layout.ts +13 -0
- package/src/identity/.spec/schemas/identity-export-v1.schema.json +25 -0
- package/src/identity/__tests__/fixtures/registry-journey.ts +53 -0
- package/src/identity/__tests__/registration.test.ts +64 -0
- package/src/identity/__tests__/registry.test.ts +70 -0
- package/src/identity/__tests__/transfer.test.ts +198 -0
- package/src/identity/index.ts +23 -0
- package/src/identity/registration.ts +72 -0
- package/src/identity/registry.ts +172 -0
- package/src/identity/transfer.ts +187 -0
- package/src/identity/tsconfig.json +6 -0
- package/src/index.ts +1 -0
- package/src/keys/.spec/api.d.ts +74 -0
- package/src/keys/.spec/architecture.md +13 -0
- package/src/keys/.spec/laws/keys.ts +43 -0
- package/src/keys/.spec/layout.ts +6 -0
- package/src/keys/__tests__/algorithm.test.ts +39 -0
- package/src/keys/__tests__/keys.test.ts +168 -0
- package/src/keys/algorithm.ts +33 -0
- package/src/keys/credential.ts +112 -0
- package/src/keys/index.ts +14 -0
- package/src/keys/pair.ts +219 -0
- package/src/keys/tsconfig.json +6 -0
- package/src/lib/__tests__/admin-target.test.ts +67 -7
- package/src/lib/__tests__/binary.test.ts +2 -3
- package/src/lib/__tests__/command-dx.test.ts +2 -2
- package/src/lib/__tests__/config.test.ts +3 -2
- package/src/lib/__tests__/idp.test.ts +4 -6
- package/src/lib/__tests__/instance-target.test.ts +7 -4
- package/src/lib/__tests__/local-status.test.ts +1 -1
- package/src/lib/__tests__/log.test.ts +31 -0
- package/src/lib/__tests__/meta.test.ts +55 -0
- package/src/lib/__tests__/sdk-deps.test.ts +1 -1
- package/src/lib/__tests__/self.test.ts +8 -230
- package/src/lib/__tests__/use-target.test.ts +1 -1
- package/src/lib/__tests__/view-open-intent.test.ts +78 -104
- package/src/lib/__tests__/view-session.test.ts +99 -0
- package/src/lib/admin-domain.ts +39 -25
- package/src/lib/admin-instance.ts +38 -43
- package/src/lib/admin-target.ts +67 -13
- package/src/lib/binary.ts +13 -34
- package/src/lib/browser.ts +1 -1
- package/src/{kernel → lib}/ca-fetch.ts +35 -27
- package/src/lib/config.ts +1 -1
- package/src/lib/idp-session.ts +1 -1
- package/src/lib/idp.ts +3 -8
- package/src/lib/instance-target.ts +12 -8
- package/src/lib/instance.ts +34 -2
- package/src/lib/local-status.ts +2 -2
- package/src/lib/log.ts +3 -1
- package/src/lib/login-flow.ts +32 -5
- package/src/lib/meta.ts +15 -11
- package/src/lib/provision-instance.ts +4 -12
- package/src/lib/self.ts +5 -147
- package/src/lib/update.ts +1 -1
- package/src/lib/use-target.ts +1 -1
- package/src/lib/validation.ts +2 -2
- package/src/lib/view/open-intent.ts +7 -37
- package/src/lib/view/port-allocation.ts +1 -1
- package/src/lib/view/resolve.ts +52 -45
- package/src/lib/view/server.ts +16 -33
- package/src/lib/view/session.ts +51 -22
- package/src/paths/.spec/api.d.ts +13 -0
- package/src/paths/.spec/architecture.md +5 -0
- package/src/paths/.spec/layout.ts +3 -0
- package/src/paths/index.ts +13 -0
- package/src/program/.spec/api.d.ts +42 -0
- package/src/program/.spec/architecture.md +18 -0
- package/src/program/.spec/laws/program.ts +29 -0
- package/src/program/.spec/layout.ts +14 -0
- package/src/program/__tests__/program.test.ts +359 -0
- package/src/program/build.ts +190 -0
- package/src/program/command.ts +42 -0
- package/src/program/index.ts +2 -0
- package/src/program/options.ts +40 -0
- package/src/{registry.ts → program/registry.ts} +5 -1
- package/src/program/tsconfig.json +6 -0
- package/src/setup/steps/admin.ts +18 -3
- package/src/setup/steps/instance.ts +1 -1
- package/src/state/.spec/api.d.ts +133 -0
- package/src/state/.spec/architecture.md +31 -0
- package/src/state/.spec/capabilities/state.ts +7 -0
- package/src/state/.spec/flows/identity-update.ts +22 -0
- package/src/state/.spec/laws/state.ts +73 -0
- package/src/state/.spec/layout.ts +14 -0
- package/src/state/__tests__/exchange-credentials.test.ts +188 -0
- package/src/state/__tests__/files.test.ts +113 -0
- package/src/state/__tests__/fixtures/identity-transition.ts +37 -0
- package/src/state/__tests__/identities.test.ts +142 -0
- package/src/state/__tests__/paths.test.ts +27 -0
- package/src/state/exchange-credentials.ts +219 -0
- package/src/state/files.ts +129 -0
- package/src/state/identities.ts +236 -0
- package/src/state/index.ts +29 -0
- package/src/state/paths.ts +61 -0
- package/src/state/tsconfig.json +6 -0
- package/src/telemetry/__tests__/redact.test.ts +3 -3
- package/src/telemetry/__tests__/trigger.test.ts +1 -1
- package/src/telemetry/settings.ts +1 -1
- package/src/telemetry/store.ts +1 -1
- package/src/test-utils.ts +1 -1
- package/studio/client/dist/assets/elk-api-D0cBetPW.js +1 -0
- package/studio/client/dist/assets/elk-worker.min-C9JGDOE-.js +6312 -0
- package/studio/client/dist/assets/index-BQnJ5sgd.css +1 -0
- package/studio/client/dist/assets/index-ChOr3yP0.css +1 -0
- package/studio/client/dist/assets/index-Dspir4w7.js +81 -0
- package/studio/client/dist/assets/index-bVD2KJgz.js +8 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +10 -1
- package/studio/server/introspect/extractor.ts +1 -1
- package/studio/server/state/views.test.ts +3 -7
- package/studio/server/state/views.ts +23 -30
- package/studio/tsconfig.json +23 -0
- package/tsconfig.json +1 -1
- package/viewer/dist/main.js +65 -39
- package/viewer/tsconfig.json +13 -0
- package/src/command.ts +0 -42
- package/src/commands/__tests__/help-contract.test.ts +0 -164
- package/src/kernel/__tests__/client-owned-lookup.test.ts +0 -51
- package/src/kernel/__tests__/errors.test.ts +0 -43
- package/src/kernel/__tests__/expand.test.ts +0 -123
- package/src/kernel/client.ts +0 -214
- package/src/kernel/expand.ts +0 -192
- package/src/kernel/graph.ts +0 -96
- package/src/kernel/index.ts +0 -29
- package/src/kernel/options.ts +0 -22
- package/src/kernel/types.ts +0 -14
- package/src/lib/__tests__/domain-identity.test.ts +0 -60
- package/src/lib/__tests__/fs-atomic.test.ts +0 -104
- package/src/lib/__tests__/identity.test.ts +0 -79
- package/src/lib/__tests__/keys.test.ts +0 -129
- package/src/lib/domain-identity.ts +0 -49
- package/src/lib/env.ts +0 -49
- package/src/lib/fs-atomic.ts +0 -126
- package/src/lib/identity.ts +0 -256
- package/src/lib/keys.ts +0 -294
- package/src/lib/paths.ts +0 -11
- package/src/program.ts +0 -213
- package/studio/client/dist/assets/index-BaqEuIQJ.js +0 -109
- package/studio/client/dist/assets/index-jRdExahh.css +0 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { fileExists, keypairPaths } from '../../../keys/index'
|
|
2
|
+
import { KEYS_DIR } from '../../../state/index'
|
|
3
|
+
import {
|
|
4
|
+
createIdentity,
|
|
5
|
+
exportIdentity,
|
|
6
|
+
importIdentity,
|
|
7
|
+
deleteIdentity,
|
|
8
|
+
getDefault,
|
|
9
|
+
readIdentities,
|
|
10
|
+
setDefault,
|
|
11
|
+
setIdentityMode,
|
|
12
|
+
setRegistration,
|
|
13
|
+
upsertIdpIdentity,
|
|
14
|
+
} from '../../index'
|
|
15
|
+
|
|
16
|
+
await createIdentity('alice')
|
|
17
|
+
await createIdentity('bob', { mode: 'remote' })
|
|
18
|
+
await importIdentity(await exportIdentity('alice'), { name: 'alice-alias' })
|
|
19
|
+
await setDefault('alice')
|
|
20
|
+
await setIdentityMode('bob', 'local')
|
|
21
|
+
await setRegistration('alice', 'production', {
|
|
22
|
+
iss: 'https://kernel.example',
|
|
23
|
+
sub: 'node-alice',
|
|
24
|
+
registeredAt: '2026-08-11T12:00:00.000Z',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
let selectedDeletion = ''
|
|
28
|
+
try {
|
|
29
|
+
await deleteIdentity('alice')
|
|
30
|
+
} catch (error) {
|
|
31
|
+
selectedDeletion = error instanceof Error ? error.message : String(error)
|
|
32
|
+
}
|
|
33
|
+
await deleteIdentity('bob')
|
|
34
|
+
await deleteIdentity('alice-alias')
|
|
35
|
+
await upsertIdpIdentity('workos', {
|
|
36
|
+
subject: 'user_123',
|
|
37
|
+
idp: 'workos',
|
|
38
|
+
issuer: 'https://idp.example',
|
|
39
|
+
audience: 'https://kernel.example',
|
|
40
|
+
use: false,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const store = await readIdentities()
|
|
44
|
+
const selected = await getDefault()
|
|
45
|
+
console.log(
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
store,
|
|
48
|
+
selected,
|
|
49
|
+
selectedDeletion,
|
|
50
|
+
aliceKey: await fileExists(keypairPaths('alice', KEYS_DIR).privatePath),
|
|
51
|
+
bobKey: await fileExists(keypairPaths('bob', KEYS_DIR).privatePath),
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ProvisionRequest } from '@astrale-os/sdk/auth'
|
|
2
|
+
|
|
3
|
+
import { LocalBinding } from '@astrale-os/sdk/graph/model'
|
|
4
|
+
import { expect, test } from 'bun:test'
|
|
5
|
+
|
|
6
|
+
import { submitIdentityProvision } from '../registration'
|
|
7
|
+
|
|
8
|
+
/** @evidence TEST-CLI-IDENTITY-REGISTER-DOMAIN-MEDIATED */
|
|
9
|
+
test('submits the exact self-proven request through an explicit Domain callable', async () => {
|
|
10
|
+
const binding = LocalBinding('identity')
|
|
11
|
+
const request = {
|
|
12
|
+
idempotencyKey: 'identity-register:operator',
|
|
13
|
+
mutation: {
|
|
14
|
+
format: 'astrale.graph.mutation',
|
|
15
|
+
version: 'v2',
|
|
16
|
+
preconditions: [],
|
|
17
|
+
operations: [],
|
|
18
|
+
},
|
|
19
|
+
identities: {
|
|
20
|
+
identity: {
|
|
21
|
+
credentials: {
|
|
22
|
+
publicKey: { kty: 'EC', crv: 'P-256', x: 'x', y: 'y', alg: 'ES256' },
|
|
23
|
+
proof: 'self-proven-request.jwt.signature',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
} as unknown as ProvisionRequest
|
|
28
|
+
const calls: unknown[] = []
|
|
29
|
+
let directCalls = 0
|
|
30
|
+
const result = await submitIdentityProvision({
|
|
31
|
+
binding,
|
|
32
|
+
request,
|
|
33
|
+
via: '/:ops.example:function.provisionOperator',
|
|
34
|
+
direct: {
|
|
35
|
+
async provision() {
|
|
36
|
+
directCalls += 1
|
|
37
|
+
throw new Error('direct provision must not run')
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
callable: {
|
|
41
|
+
async call(call) {
|
|
42
|
+
calls.push(call)
|
|
43
|
+
return {
|
|
44
|
+
createdNodes: { identity: 'operator-node' },
|
|
45
|
+
identities: {
|
|
46
|
+
identity: { issuer: 'https://identity.example', subject: 'operator-node' },
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
expect(directCalls).toBe(0)
|
|
54
|
+
expect(calls).toHaveLength(1)
|
|
55
|
+
expect(JSON.parse(JSON.stringify(calls[0]))).toEqual({
|
|
56
|
+
target: '/:ops.example:function.provisionOperator',
|
|
57
|
+
input: JSON.parse(JSON.stringify(request)),
|
|
58
|
+
})
|
|
59
|
+
expect(result).toEqual({
|
|
60
|
+
iss: 'https://identity.example',
|
|
61
|
+
sub: 'operator-node',
|
|
62
|
+
nodeId: 'operator-node',
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
describe('identity registry journey', () => {
|
|
7
|
+
let root = ''
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
root = await mkdtemp(join(tmpdir(), 'astrale-identity-registry-'))
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await rm(root, { recursive: true, force: true })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
/** @evidence TEST-CLI-IDENTITY-REGISTRY-JOURNEY */
|
|
18
|
+
test('runs create, selection, registration, mode, IdP, and deletion in a real process', async () => {
|
|
19
|
+
const child = Bun.spawn(
|
|
20
|
+
[process.execPath, join(import.meta.dir, 'fixtures', 'registry-journey.ts')],
|
|
21
|
+
{
|
|
22
|
+
env: { ...process.env, ASTRALE_HOME: root },
|
|
23
|
+
stdout: 'pipe',
|
|
24
|
+
stderr: 'pipe',
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
28
|
+
child.exited,
|
|
29
|
+
new Response(child.stdout).text(),
|
|
30
|
+
new Response(child.stderr).text(),
|
|
31
|
+
])
|
|
32
|
+
expect(exitCode, stderr).toBe(0)
|
|
33
|
+
|
|
34
|
+
const result = JSON.parse(stdout) as {
|
|
35
|
+
store: {
|
|
36
|
+
default: string
|
|
37
|
+
identities: Record<
|
|
38
|
+
string,
|
|
39
|
+
{
|
|
40
|
+
source?: string
|
|
41
|
+
mode?: string
|
|
42
|
+
registrations?: Record<string, { iss: string; sub: string; registeredAt: string }>
|
|
43
|
+
}
|
|
44
|
+
>
|
|
45
|
+
}
|
|
46
|
+
selected: { name: string; subject: string }
|
|
47
|
+
selectedDeletion: string
|
|
48
|
+
aliceKey: boolean
|
|
49
|
+
bobKey: boolean
|
|
50
|
+
}
|
|
51
|
+
expect(result.store.default).toBe('alice')
|
|
52
|
+
expect(Object.keys(result.store.identities).sort()).toEqual(['alice', 'manager', 'workos'])
|
|
53
|
+
expect(result.store.identities.alice.registrations?.production).toEqual({
|
|
54
|
+
iss: 'https://kernel.example',
|
|
55
|
+
sub: 'node-alice',
|
|
56
|
+
registeredAt: '2026-08-11T12:00:00.000Z',
|
|
57
|
+
})
|
|
58
|
+
expect(result.store.identities.workos).toMatchObject({ source: 'idp', mode: 'remote' })
|
|
59
|
+
expect(result.selected).toMatchObject({ name: 'alice', subject: 'alice' })
|
|
60
|
+
expect(result.selectedDeletion).toContain('Cannot delete the default identity')
|
|
61
|
+
expect(result.aliceKey).toBe(true)
|
|
62
|
+
expect(result.bobKey).toBe(false)
|
|
63
|
+
|
|
64
|
+
const persisted = JSON.parse(await readFile(join(root, 'identities.json'), 'utf-8')) as {
|
|
65
|
+
version?: unknown
|
|
66
|
+
}
|
|
67
|
+
expect(persisted.version).toBe(1)
|
|
68
|
+
expect(stdout).not.toContain('"d":')
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { access, mkdtemp, readFile, readdir, rm, stat } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { keypairPaths, persistKeypair, readKeypair } from '../../keys/index'
|
|
8
|
+
import { readIdentityStore, updateIdentityStore } from '../../state/index'
|
|
9
|
+
import {
|
|
10
|
+
decodeIdentityExport,
|
|
11
|
+
encodeIdentityExport,
|
|
12
|
+
exportIdentity,
|
|
13
|
+
importIdentity,
|
|
14
|
+
isEncryptedIdentityExport,
|
|
15
|
+
writeIdentityExport,
|
|
16
|
+
type IdentityExport,
|
|
17
|
+
} from '../index'
|
|
18
|
+
|
|
19
|
+
describe('identity transfer', () => {
|
|
20
|
+
let root = ''
|
|
21
|
+
let statePath = ''
|
|
22
|
+
let keysDir = ''
|
|
23
|
+
const now = () => new Date('2026-08-11T12:00:00.000Z')
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
root = await mkdtemp(join(tmpdir(), 'astrale-identity-transfer-'))
|
|
27
|
+
statePath = join(root, 'identities.json')
|
|
28
|
+
keysDir = join(root, 'keys')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
await rm(root, { recursive: true, force: true })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
async function envelope(subject: string, suffix: string): Promise<IdentityExport> {
|
|
36
|
+
const pair = await persistKeypair(subject, {
|
|
37
|
+
keysDir: join(root, `source-${suffix}`),
|
|
38
|
+
kid: `${subject}-${suffix}`,
|
|
39
|
+
})
|
|
40
|
+
return decodeIdentityExport(
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
version: 1,
|
|
43
|
+
subject,
|
|
44
|
+
mode: 'local',
|
|
45
|
+
kid: pair.kid,
|
|
46
|
+
issuer: 'https://identity.example',
|
|
47
|
+
privateJwk: pair.privateJwk,
|
|
48
|
+
publicJwk: pair.publicJwk,
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @evidence TEST-CLI-IDENTITY-TRANSFER-REJECTS */
|
|
54
|
+
test('rejects malformed, newer, and mismatched content before durable mutation', async () => {
|
|
55
|
+
await expect(decodeIdentityExport('{')).rejects.toThrow(/not valid JSON/)
|
|
56
|
+
await expect(
|
|
57
|
+
decodeIdentityExport(
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
version: 2,
|
|
60
|
+
subject: 'alice',
|
|
61
|
+
mode: 'local',
|
|
62
|
+
privateJwk: {},
|
|
63
|
+
publicJwk: {},
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
).rejects.toThrow(/invalid or unsupported shape/)
|
|
67
|
+
|
|
68
|
+
const alice = await envelope('alice', 'a')
|
|
69
|
+
const bob = await envelope('bob', 'b')
|
|
70
|
+
const forged = {
|
|
71
|
+
...alice,
|
|
72
|
+
publicJwk: { ...bob.publicJwk, kid: alice.kid },
|
|
73
|
+
} as IdentityExport
|
|
74
|
+
const mismatch = await importIdentity(forged, {
|
|
75
|
+
state: { path: statePath, now },
|
|
76
|
+
keysDir,
|
|
77
|
+
}).catch((error) => error)
|
|
78
|
+
expect(mismatch).toBeInstanceOf(Error)
|
|
79
|
+
expect((mismatch as Error).message).toMatch(/do not form a valid matching pair/)
|
|
80
|
+
expect((mismatch as Error).message).not.toContain(String(alice.privateJwk.d))
|
|
81
|
+
|
|
82
|
+
await expect(access(statePath)).rejects.toThrow()
|
|
83
|
+
await expect(access(keypairPaths('alice', keysDir).privatePath)).rejects.toThrow()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
/** @evidence TEST-CLI-IDENTITY-TRANSFER-ROUNDTRIP */
|
|
87
|
+
test('converges legacy, V1, and encrypted representations on one envelope', async () => {
|
|
88
|
+
const current = await envelope('alice', 'roundtrip')
|
|
89
|
+
const { version: _, ...legacy } = current
|
|
90
|
+
expect(await decodeIdentityExport(JSON.stringify(legacy))).toEqual(current)
|
|
91
|
+
|
|
92
|
+
const plaintext = await encodeIdentityExport(current)
|
|
93
|
+
expect(isEncryptedIdentityExport(plaintext)).toBe(false)
|
|
94
|
+
expect(await decodeIdentityExport(plaintext)).toEqual(current)
|
|
95
|
+
|
|
96
|
+
const encrypted = await encodeIdentityExport(current, 'correct-horse-battery')
|
|
97
|
+
expect(isEncryptedIdentityExport(encrypted)).toBe(true)
|
|
98
|
+
expect(await decodeIdentityExport(encrypted, 'correct-horse-battery')).toEqual(current)
|
|
99
|
+
await expect(decodeIdentityExport(encrypted, 'wrong')).rejects.toThrow(/Could not decrypt/)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
/** @evidence TEST-CLI-IDENTITY-TRANSFER-SCHEMA */
|
|
103
|
+
test('keeps the portable V1 JSON Schema aligned with the admitted identity envelope', async () => {
|
|
104
|
+
const schemaPath = fileURLToPath(
|
|
105
|
+
new URL('../.spec/schemas/identity-export-v1.schema.json', import.meta.url),
|
|
106
|
+
)
|
|
107
|
+
const schema = JSON.parse(await readFile(schemaPath, 'utf8')) as {
|
|
108
|
+
readonly $id: string
|
|
109
|
+
readonly additionalProperties: boolean
|
|
110
|
+
readonly required: readonly string[]
|
|
111
|
+
readonly properties: Readonly<Record<string, unknown>>
|
|
112
|
+
}
|
|
113
|
+
const identityEnvelope = await envelope('manager-principal', 'host-bootstrap')
|
|
114
|
+
|
|
115
|
+
expect(schema.$id).toBe('https://schemas.astrale.ai/cli/identity-export/1')
|
|
116
|
+
expect(schema.additionalProperties).toBe(false)
|
|
117
|
+
expect(schema.required).toEqual(['version', 'subject', 'privateJwk', 'publicJwk'])
|
|
118
|
+
expect(Object.keys(schema.properties).sort()).toEqual(
|
|
119
|
+
['issuer', 'kid', 'mode', 'privateJwk', 'publicJwk', 'subject', 'version'].sort(),
|
|
120
|
+
)
|
|
121
|
+
await expect(decodeIdentityExport(JSON.stringify(identityEnvelope))).resolves.toEqual(
|
|
122
|
+
identityEnvelope,
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
/** @evidence TEST-CLI-IDENTITY-IMPORT-ORDERED */
|
|
127
|
+
test('checks conflicts before key replacement and publishes registry after keys', async () => {
|
|
128
|
+
const first = await envelope('alice', 'first')
|
|
129
|
+
const imported = await importIdentity(first, {
|
|
130
|
+
name: 'operator',
|
|
131
|
+
state: { path: statePath, now },
|
|
132
|
+
keysDir,
|
|
133
|
+
})
|
|
134
|
+
expect(imported.subject).toBe('alice')
|
|
135
|
+
expect((await readKeypair('alice', keysDir)).kid).toBe(first.kid)
|
|
136
|
+
expect((await readIdentityStore({ path: statePath })).identities.operator).toEqual(imported)
|
|
137
|
+
|
|
138
|
+
const privatePath = keypairPaths('alice', keysDir).privatePath
|
|
139
|
+
const originalPrivate = await readFile(privatePath, 'utf-8')
|
|
140
|
+
const replacement = await envelope('alice', 'replacement')
|
|
141
|
+
await expect(
|
|
142
|
+
importIdentity(replacement, {
|
|
143
|
+
name: 'operator',
|
|
144
|
+
state: { path: statePath, now },
|
|
145
|
+
keysDir,
|
|
146
|
+
}),
|
|
147
|
+
).rejects.toThrow('already exists')
|
|
148
|
+
expect(await readFile(privatePath, 'utf-8')).toBe(originalPrivate)
|
|
149
|
+
|
|
150
|
+
await updateIdentityStore(
|
|
151
|
+
(store) => ({
|
|
152
|
+
next: {
|
|
153
|
+
...store,
|
|
154
|
+
identities: {
|
|
155
|
+
...store.identities,
|
|
156
|
+
remote: {
|
|
157
|
+
subject: 'remote-user',
|
|
158
|
+
createdAt: now().toISOString(),
|
|
159
|
+
source: 'idp',
|
|
160
|
+
mode: 'remote',
|
|
161
|
+
idp: 'workos',
|
|
162
|
+
issuer: 'https://idp.example',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
value: undefined,
|
|
167
|
+
}),
|
|
168
|
+
{ path: statePath, now },
|
|
169
|
+
)
|
|
170
|
+
await expect(
|
|
171
|
+
importIdentity(await envelope('remote-user', 'remote'), {
|
|
172
|
+
name: 'remote',
|
|
173
|
+
replace: true,
|
|
174
|
+
state: { path: statePath, now },
|
|
175
|
+
keysDir,
|
|
176
|
+
}),
|
|
177
|
+
).rejects.toThrow(/IdP-backed/)
|
|
178
|
+
await expect(access(keypairPaths('remote-user', keysDir).privatePath)).rejects.toThrow()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
/** @evidence TEST-CLI-IDENTITY-EXPORT-PRIVATE */
|
|
182
|
+
test('exports a proven pair through one atomic mode-0600 file', async () => {
|
|
183
|
+
await importIdentity(await envelope('alice', 'export'), {
|
|
184
|
+
state: { path: statePath, now },
|
|
185
|
+
keysDir,
|
|
186
|
+
})
|
|
187
|
+
const exported = await exportIdentity('alice', {
|
|
188
|
+
state: { path: statePath, now },
|
|
189
|
+
keysDir,
|
|
190
|
+
})
|
|
191
|
+
const path = join(root, 'alice.identity.json')
|
|
192
|
+
await writeIdentityExport(path, await encodeIdentityExport(exported))
|
|
193
|
+
|
|
194
|
+
expect((await stat(path)).mode & 0o777).toBe(0o600)
|
|
195
|
+
expect(await decodeIdentityExport(await readFile(path, 'utf-8'))).toEqual(exported)
|
|
196
|
+
expect((await readdir(root)).some((entry) => entry.endsWith('.tmp'))).toBe(false)
|
|
197
|
+
})
|
|
198
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createIdentity,
|
|
3
|
+
deleteIdentity,
|
|
4
|
+
getDefault,
|
|
5
|
+
getIdentity,
|
|
6
|
+
readIdentities,
|
|
7
|
+
setDefault,
|
|
8
|
+
setIdentityMode,
|
|
9
|
+
setRegistration,
|
|
10
|
+
upsertIdpIdentity,
|
|
11
|
+
} from './registry'
|
|
12
|
+
export {
|
|
13
|
+
decodeIdentityExport,
|
|
14
|
+
encodeIdentityExport,
|
|
15
|
+
exportIdentity,
|
|
16
|
+
importIdentity,
|
|
17
|
+
isEncryptedIdentityExport,
|
|
18
|
+
writeIdentityExport,
|
|
19
|
+
} from './transfer'
|
|
20
|
+
export { acceptProvisionedIdentity, submitIdentityProvision } from './registration'
|
|
21
|
+
export type { IdentityProvisionSubmission, IdentityRegistrationResult } from './registration'
|
|
22
|
+
export type { Identity, IdentityStore, Registration } from './registry'
|
|
23
|
+
export type { IdentityExport, IdentityFileOptions, IdentityImportOptions } from './transfer'
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Call } from '@astrale-os/kernel-client'
|
|
2
|
+
import type { ProvisionRequest } from '@astrale-os/sdk/auth'
|
|
3
|
+
import type { LocalBinding } from '@astrale-os/sdk/graph/model'
|
|
4
|
+
|
|
5
|
+
import { call } from '@astrale-os/kernel-client'
|
|
6
|
+
import { issuer } from '@astrale-os/sdk/auth'
|
|
7
|
+
import { NodeId } from '@astrale-os/sdk/graph/node'
|
|
8
|
+
import { Path } from '@astrale-os/sdk/graph/path'
|
|
9
|
+
|
|
10
|
+
export interface IdentityRegistrationResult {
|
|
11
|
+
readonly iss: string
|
|
12
|
+
readonly sub: string
|
|
13
|
+
readonly nodeId?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IdentityProvisionSubmission {
|
|
17
|
+
readonly request: ProvisionRequest
|
|
18
|
+
readonly binding: LocalBinding
|
|
19
|
+
readonly via?: string
|
|
20
|
+
readonly direct: {
|
|
21
|
+
provision(request: ProvisionRequest): Promise<unknown>
|
|
22
|
+
}
|
|
23
|
+
readonly callable: {
|
|
24
|
+
call(call: Call): Promise<unknown>
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Submit one prepared request either directly or through its explicit Domain authority owner. */
|
|
29
|
+
export async function submitIdentityProvision(
|
|
30
|
+
input: IdentityProvisionSubmission,
|
|
31
|
+
): Promise<IdentityRegistrationResult> {
|
|
32
|
+
const result =
|
|
33
|
+
input.via === undefined
|
|
34
|
+
? await input.direct.provision(input.request)
|
|
35
|
+
: await input.callable.call(
|
|
36
|
+
// Client admits the portable value before transport. ProvisionRequest is the
|
|
37
|
+
// stricter semantic type but does not declare the portable Object index signature.
|
|
38
|
+
call(Path.parse(input.via), input.request as unknown as Call['input']),
|
|
39
|
+
)
|
|
40
|
+
return acceptProvisionedIdentity(result, input.binding)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Admit only the exact binding the CLI prepared; remote callables remain untrusted input. */
|
|
44
|
+
export function acceptProvisionedIdentity(
|
|
45
|
+
value: unknown,
|
|
46
|
+
binding: LocalBinding,
|
|
47
|
+
): IdentityRegistrationResult {
|
|
48
|
+
const result = record(value, 'Provision result')
|
|
49
|
+
const identities = record(result.identities, 'Provision result identities')
|
|
50
|
+
const identity = record(identities[binding], 'Provision result identity binding')
|
|
51
|
+
const createdNodes = record(result.createdNodes, 'Provision result created Nodes')
|
|
52
|
+
const node = createdNodes[binding]
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
iss: issuer.accept(text(identity.issuer, 'Provisioned Identity issuer')),
|
|
55
|
+
sub: text(identity.subject, 'Provisioned Identity subject'),
|
|
56
|
+
...(node === undefined ? {} : { nodeId: NodeId(text(node, 'Provisioned Identity Node')) }),
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function record(value: unknown, label: string): Record<string, unknown> {
|
|
61
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
62
|
+
throw new TypeError(`${label} must be an object.`)
|
|
63
|
+
}
|
|
64
|
+
return value as Record<string, unknown>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function text(value: unknown, label: string): string {
|
|
68
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
69
|
+
throw new TypeError(`${label} must be non-empty text.`)
|
|
70
|
+
}
|
|
71
|
+
return value
|
|
72
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { persistKeypair, removeKeypair } from '../keys/index'
|
|
2
|
+
import { deleteIdpSession } from '../lib/idp'
|
|
3
|
+
import { validateName } from '../lib/validation'
|
|
4
|
+
import {
|
|
5
|
+
readIdentityStore,
|
|
6
|
+
updateIdentityStore,
|
|
7
|
+
type Identity,
|
|
8
|
+
type IdentityMode,
|
|
9
|
+
type IdentityStore,
|
|
10
|
+
type Registration,
|
|
11
|
+
ExchangeCredentialCache,
|
|
12
|
+
} from '../state/index'
|
|
13
|
+
|
|
14
|
+
export type { Identity, IdentityStore, Registration } from '../state/index'
|
|
15
|
+
|
|
16
|
+
export async function readIdentities(): Promise<IdentityStore> {
|
|
17
|
+
return readIdentityStore()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function createIdentity(
|
|
21
|
+
name: string,
|
|
22
|
+
options: {
|
|
23
|
+
readonly subject?: string
|
|
24
|
+
readonly mode?: IdentityMode
|
|
25
|
+
readonly issuer?: string
|
|
26
|
+
readonly kid?: string
|
|
27
|
+
} = {},
|
|
28
|
+
): Promise<Identity> {
|
|
29
|
+
validateName(name, 'Identity')
|
|
30
|
+
return updateIdentityStore(async (store) => {
|
|
31
|
+
if (store.identities[name]) throw new Error(`Identity "${name}" already exists`)
|
|
32
|
+
|
|
33
|
+
const subject = options.subject ?? name
|
|
34
|
+
const generated = await persistKeypair(subject, { kid: options.kid })
|
|
35
|
+
const identity: Identity = {
|
|
36
|
+
subject,
|
|
37
|
+
createdAt: new Date().toISOString(),
|
|
38
|
+
source: 'key',
|
|
39
|
+
mode: options.mode ?? 'local',
|
|
40
|
+
kid: generated.kid,
|
|
41
|
+
issuer: options.issuer,
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
next: { ...store, identities: { ...store.identities, [name]: identity } },
|
|
45
|
+
value: identity,
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function deleteIdentity(name: string): Promise<void> {
|
|
51
|
+
await updateIdentityStore(async (store) => {
|
|
52
|
+
const entry = store.identities[name]
|
|
53
|
+
if (!entry) throw new Error(`Identity "${name}" not found`)
|
|
54
|
+
if (store.default === name) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Cannot delete the default identity "${name}". Switch default first with: astrale identity use <other>`,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
if ((entry.source ?? 'key') === 'idp') await deleteIdpSession(name)
|
|
60
|
+
else if (!hasAnotherKeyIdentity(store, name, entry.subject)) await removeKeypair(entry.subject)
|
|
61
|
+
|
|
62
|
+
const { [name]: _, ...identities } = store.identities
|
|
63
|
+
return { next: { ...store, identities }, value: undefined }
|
|
64
|
+
})
|
|
65
|
+
await new ExchangeCredentialCache().clear()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function hasAnotherKeyIdentity(store: IdentityStore, name: string, subject: string): boolean {
|
|
69
|
+
return Object.entries(store.identities).some(
|
|
70
|
+
([candidateName, candidate]) =>
|
|
71
|
+
candidateName !== name &&
|
|
72
|
+
(candidate.source ?? 'key') === 'key' &&
|
|
73
|
+
candidate.subject === subject,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function setDefault(name: string): Promise<void> {
|
|
78
|
+
await updateIdentityStore((store) => {
|
|
79
|
+
if (!store.identities[name]) throw new Error(`Identity "${name}" not found`)
|
|
80
|
+
return { next: { ...store, default: name }, value: undefined }
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function getDefault(): Promise<Identity & { readonly name: string }> {
|
|
85
|
+
const store = await readIdentities()
|
|
86
|
+
const identity = store.identities[store.default]
|
|
87
|
+
if (!identity) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Default identity "${store.default}" not found. Run: astrale identity create ${store.default}`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
return { ...identity, name: store.default }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function getIdentity(name: string): Promise<Identity> {
|
|
96
|
+
const store = await readIdentities()
|
|
97
|
+
const identity = store.identities[name]
|
|
98
|
+
if (!identity) {
|
|
99
|
+
throw new Error(`Identity "${name}" not found. Run: astrale identity create ${name}`)
|
|
100
|
+
}
|
|
101
|
+
return identity
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function upsertIdpIdentity(
|
|
105
|
+
name: string,
|
|
106
|
+
options: {
|
|
107
|
+
readonly subject: string
|
|
108
|
+
readonly idp: string
|
|
109
|
+
readonly issuer: string
|
|
110
|
+
readonly audience?: string
|
|
111
|
+
readonly claims?: Readonly<Record<string, unknown>>
|
|
112
|
+
readonly use?: boolean
|
|
113
|
+
},
|
|
114
|
+
): Promise<Identity> {
|
|
115
|
+
validateName(name, 'Identity')
|
|
116
|
+
validateName(options.idp, 'IdP')
|
|
117
|
+
return updateIdentityStore((store) => {
|
|
118
|
+
const existing = store.identities[name]
|
|
119
|
+
if (existing && (existing.source ?? 'key') !== 'idp') {
|
|
120
|
+
throw new Error(`Identity "${name}" already exists and is key-backed`)
|
|
121
|
+
}
|
|
122
|
+
const identity: Identity = {
|
|
123
|
+
subject: options.subject,
|
|
124
|
+
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
125
|
+
source: 'idp',
|
|
126
|
+
mode: 'remote',
|
|
127
|
+
idp: options.idp,
|
|
128
|
+
issuer: options.issuer,
|
|
129
|
+
audience: options.audience,
|
|
130
|
+
claims: options.claims,
|
|
131
|
+
registrations: existing?.registrations,
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
next: {
|
|
135
|
+
default: options.use === false ? store.default : name,
|
|
136
|
+
identities: { ...store.identities, [name]: identity },
|
|
137
|
+
},
|
|
138
|
+
value: identity,
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function setRegistration(
|
|
144
|
+
name: string,
|
|
145
|
+
instanceSlug: string,
|
|
146
|
+
registration: Registration,
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
await updateIdentityStore((store) => {
|
|
149
|
+
const entry = store.identities[name]
|
|
150
|
+
if (!entry) throw new Error(`Identity "${name}" not found`)
|
|
151
|
+
const identity: Identity = {
|
|
152
|
+
...entry,
|
|
153
|
+
registrations: { ...entry.registrations, [instanceSlug]: registration },
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
next: { ...store, identities: { ...store.identities, [name]: identity } },
|
|
157
|
+
value: undefined,
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function setIdentityMode(name: string, mode: IdentityMode): Promise<void> {
|
|
163
|
+
await updateIdentityStore((store) => {
|
|
164
|
+
const entry = store.identities[name]
|
|
165
|
+
if (!entry) throw new Error(`Identity "${name}" not found`)
|
|
166
|
+
const identity: Identity = { ...entry, mode }
|
|
167
|
+
return {
|
|
168
|
+
next: { ...store, identities: { ...store.identities, [name]: identity } },
|
|
169
|
+
value: undefined,
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
}
|