@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
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { CompactEncrypt, compactDecrypt, decodeProtectedHeader } from 'jose'
|
|
3
|
-
import { access, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
4
|
-
import { tmpdir } from 'node:os'
|
|
5
|
-
import { join } from 'node:path'
|
|
6
|
-
|
|
7
|
-
import { IdentityKeyMissingError } from '../../errors'
|
|
8
|
-
import {
|
|
9
|
-
generateEd25519Jwk,
|
|
10
|
-
keypairPaths,
|
|
11
|
-
listIdentityKeys,
|
|
12
|
-
persistKeypair,
|
|
13
|
-
removeKeypair,
|
|
14
|
-
signAs,
|
|
15
|
-
} from '../keys'
|
|
16
|
-
|
|
17
|
-
describe('DESIGN — per-identity keys', () => {
|
|
18
|
-
let tmp = ''
|
|
19
|
-
const savedStrict = process.env.ASTRALE_STRICT_IDENTITIES
|
|
20
|
-
|
|
21
|
-
beforeEach(async () => {
|
|
22
|
-
tmp = await mkdtemp(join(tmpdir(), 'astrale-keys-test-'))
|
|
23
|
-
delete process.env.ASTRALE_STRICT_IDENTITIES
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
afterEach(async () => {
|
|
27
|
-
if (savedStrict) process.env.ASTRALE_STRICT_IDENTITIES = savedStrict
|
|
28
|
-
else delete process.env.ASTRALE_STRICT_IDENTITIES
|
|
29
|
-
await rm(tmp, { recursive: true, force: true })
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('keypairPaths routes manager to legacy filenames', () => {
|
|
33
|
-
const p = keypairPaths('manager', tmp)
|
|
34
|
-
expect(p.privatePath.endsWith('manager.private.jwk')).toBe(true)
|
|
35
|
-
expect(p.publicPath.endsWith('manager.public.jwk')).toBe(true)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
test('keypairPaths routes others to <name>.*.jwk', () => {
|
|
39
|
-
const p = keypairPaths('alice', tmp)
|
|
40
|
-
expect(p.privatePath.endsWith('alice.private.jwk')).toBe(true)
|
|
41
|
-
expect(p.publicPath.endsWith('alice.public.jwk')).toBe(true)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
test('persistKeypair writes both files with kid', async () => {
|
|
45
|
-
const result = await persistKeypair('alice', { keysDir: tmp })
|
|
46
|
-
expect(result.kid).toMatch(/^alice-key-/)
|
|
47
|
-
const { privatePath, publicPath } = keypairPaths('alice', tmp)
|
|
48
|
-
await access(privatePath)
|
|
49
|
-
await access(publicPath)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
test('signAs(alice) works when alice has her own key', async () => {
|
|
53
|
-
await persistKeypair('alice', { keysDir: tmp })
|
|
54
|
-
const jwt = await signAs('alice', tmp)
|
|
55
|
-
expect(jwt.split('.').length).toBe(3)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test('signAs(alice) uses the key file algorithm for EdDSA identities', async () => {
|
|
59
|
-
const { privateJwk, publicJwk } = await generateEd25519Jwk('alice-ed25519')
|
|
60
|
-
const { privatePath, publicPath } = keypairPaths('alice', tmp)
|
|
61
|
-
await writeFile(privatePath, JSON.stringify(privateJwk, null, 2))
|
|
62
|
-
await writeFile(publicPath, JSON.stringify(publicJwk, null, 2))
|
|
63
|
-
|
|
64
|
-
const jwt = await signAs('alice', tmp)
|
|
65
|
-
|
|
66
|
-
expect(decodeProtectedHeader(jwt).alg).toBe('EdDSA')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test('signAs(alice) falls back to manager key with warning', async () => {
|
|
70
|
-
await persistKeypair('manager', { keysDir: tmp })
|
|
71
|
-
const jwt = await signAs('alice', tmp)
|
|
72
|
-
expect(jwt.split('.').length).toBe(3)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test('signAs(alice) errors when strict mode and no alice key', async () => {
|
|
76
|
-
await persistKeypair('manager', { keysDir: tmp })
|
|
77
|
-
process.env.ASTRALE_STRICT_IDENTITIES = '1'
|
|
78
|
-
await expect(signAs('alice', tmp)).rejects.toThrow(IdentityKeyMissingError)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
test('signAs errors when no keys at all', async () => {
|
|
82
|
-
await expect(signAs('manager', tmp)).rejects.toThrow(IdentityKeyMissingError)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
test('removeKeypair is idempotent', async () => {
|
|
86
|
-
await persistKeypair('alice', { keysDir: tmp })
|
|
87
|
-
await removeKeypair('alice', tmp)
|
|
88
|
-
await removeKeypair('alice', tmp)
|
|
89
|
-
const { privatePath } = keypairPaths('alice', tmp)
|
|
90
|
-
await expect(access(privatePath)).rejects.toThrow()
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
test('listIdentityKeys returns names with private keys', async () => {
|
|
94
|
-
await persistKeypair('manager', { keysDir: tmp })
|
|
95
|
-
await persistKeypair('alice', { keysDir: tmp })
|
|
96
|
-
await persistKeypair('bob', { keysDir: tmp })
|
|
97
|
-
const names = await listIdentityKeys(tmp)
|
|
98
|
-
expect(names.sort()).toEqual(['alice', 'bob', 'manager'])
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
test('encrypted export envelope round-trip (JWE PBES2)', async () => {
|
|
102
|
-
await persistKeypair('alice', { keysDir: tmp })
|
|
103
|
-
const plain = JSON.stringify({ subject: 'alice', secret: 'x' })
|
|
104
|
-
const passphrase = 'correct-horse-battery'
|
|
105
|
-
const enc = await new CompactEncrypt(new TextEncoder().encode(plain))
|
|
106
|
-
.setProtectedHeader({ alg: 'PBES2-HS256+A128KW', enc: 'A256GCM' })
|
|
107
|
-
.encrypt(new TextEncoder().encode(passphrase))
|
|
108
|
-
|
|
109
|
-
expect(enc.split('.').length).toBe(5)
|
|
110
|
-
|
|
111
|
-
const { plaintext } = await compactDecrypt(enc, new TextEncoder().encode(passphrase), {
|
|
112
|
-
keyManagementAlgorithms: ['PBES2-HS256+A128KW'],
|
|
113
|
-
})
|
|
114
|
-
expect(new TextDecoder().decode(plaintext)).toBe(plain)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
test('encrypted export fails with wrong passphrase', async () => {
|
|
118
|
-
const plain = JSON.stringify({ subject: 'alice' })
|
|
119
|
-
const enc = await new CompactEncrypt(new TextEncoder().encode(plain))
|
|
120
|
-
.setProtectedHeader({ alg: 'PBES2-HS256+A128KW', enc: 'A256GCM' })
|
|
121
|
-
.encrypt(new TextEncoder().encode('correct'))
|
|
122
|
-
|
|
123
|
-
await expect(
|
|
124
|
-
compactDecrypt(enc, new TextEncoder().encode('wrong'), {
|
|
125
|
-
keyManagementAlgorithms: ['PBES2-HS256+A128KW'],
|
|
126
|
-
}),
|
|
127
|
-
).rejects.toThrow()
|
|
128
|
-
})
|
|
129
|
-
})
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
|
-
|
|
4
|
-
import { AstraleError } from '../errors'
|
|
5
|
-
|
|
6
|
-
export async function loadPrivateJwk(keyPath: string): Promise<Record<string, unknown>> {
|
|
7
|
-
const filePath = resolve(keyPath)
|
|
8
|
-
const raw = await readFile(filePath, 'utf-8')
|
|
9
|
-
return JSON.parse(raw) as Record<string, unknown>
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* True when the kernel rejected a `Domain.install` call because the
|
|
14
|
-
* identity binding's signature failed to verify — i.e. the private/public
|
|
15
|
-
* JWK halves don't match. Callers re-throw with a regenerate-the-keypair
|
|
16
|
-
* hint instead of leaking the raw `AuthenticationError`.
|
|
17
|
-
*/
|
|
18
|
-
export function isSignatureVerificationError(e: unknown): boolean {
|
|
19
|
-
if (!e || typeof e !== 'object') return false
|
|
20
|
-
const name = (e as { name?: unknown }).name
|
|
21
|
-
const msg = (e as { message?: unknown }).message
|
|
22
|
-
return (
|
|
23
|
-
name === 'AuthenticationError' &&
|
|
24
|
-
typeof msg === 'string' &&
|
|
25
|
-
/signature verification failed/i.test(msg)
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Resolve the JOSE algorithm to use with this key. Prefers `privateJwk.alg`
|
|
31
|
-
* when present, falls back to inferring from `crv`/`kty` so keys generated
|
|
32
|
-
* by older CLIs (which didn't stamp `alg`) keep working without manual
|
|
33
|
-
* editing — see META_TRACE #34. Throws a clean error if neither path
|
|
34
|
-
* resolves an algorithm.
|
|
35
|
-
*/
|
|
36
|
-
export function inferAlg(privateJwk: Record<string, unknown>, keyPath?: string): string {
|
|
37
|
-
const explicit = privateJwk.alg
|
|
38
|
-
if (typeof explicit === 'string' && explicit.length > 0) return explicit
|
|
39
|
-
const crv = privateJwk.crv
|
|
40
|
-
const kty = privateJwk.kty
|
|
41
|
-
if (kty === 'EC' && crv === 'P-256') return 'ES256'
|
|
42
|
-
if (kty === 'OKP' && crv === 'Ed25519') return 'EdDSA'
|
|
43
|
-
const where = keyPath ? ` at ${keyPath}` : ''
|
|
44
|
-
throw new AstraleError(
|
|
45
|
-
'INVALID_KEY_FILE',
|
|
46
|
-
`JWK${where} is missing both \`alg\` and a recognizable \`(kty, crv)\` pair — cannot pick a signing algorithm.`,
|
|
47
|
-
'Re-stamp the file with `"alg": "ES256"` (P-256 EC keys) or `"alg": "EdDSA"` (Ed25519 OKP keys), or regenerate via `astrale domain init` / `astrale identity create`.',
|
|
48
|
-
)
|
|
49
|
-
}
|
package/src/lib/env.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { homedir } from 'node:os'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
|
|
4
|
-
export type Paths = {
|
|
5
|
-
home: string
|
|
6
|
-
keys: string
|
|
7
|
-
data: string
|
|
8
|
-
config: string
|
|
9
|
-
install: string
|
|
10
|
-
identities: string
|
|
11
|
-
instances: string
|
|
12
|
-
idps: string
|
|
13
|
-
idpSessionsDir: string
|
|
14
|
-
/** Per-IdP config dir: `~/.astrale/idps/<name>/`. */
|
|
15
|
-
idpDir: (name: string) => string
|
|
16
|
-
/** Per-identity IdP session cache: `~/.astrale/idp-sessions/<name>.json`. */
|
|
17
|
-
idpSession: (identityName: string) => string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Resolve the Astrale home dir.
|
|
22
|
-
*
|
|
23
|
-
* Priority: explicit arg → `ASTRALE_HOME` env var → `$HOME/.astrale`.
|
|
24
|
-
*/
|
|
25
|
-
function resolveHome(home?: string): string {
|
|
26
|
-
return home ?? process.env.ASTRALE_HOME ?? join(homedir(), '.astrale')
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function createPaths(home?: string): Paths {
|
|
30
|
-
const base = resolveHome(home)
|
|
31
|
-
const idpsDir = join(base, 'idps')
|
|
32
|
-
const idpSessionsDir = join(base, 'idp-sessions')
|
|
33
|
-
return {
|
|
34
|
-
home: base,
|
|
35
|
-
keys: process.env.ASTRALE_KEYS_DIR ?? join(base, 'keys'),
|
|
36
|
-
data: process.env.ASTRALE_DATA_DIR ?? join(base, 'data'),
|
|
37
|
-
config: join(base, 'config.json'),
|
|
38
|
-
install: join(base, 'install.json'),
|
|
39
|
-
identities: join(base, 'identities.json'),
|
|
40
|
-
instances: join(base, 'instances.json'),
|
|
41
|
-
idps: join(idpsDir, 'index.json'),
|
|
42
|
-
idpSessionsDir,
|
|
43
|
-
idpDir: (name: string) => join(idpsDir, name),
|
|
44
|
-
idpSession: (identityName: string) => join(idpSessionsDir, `${identityName}.json`),
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Default singleton used by all lib modules. */
|
|
49
|
-
export const paths: Paths = createPaths()
|
package/src/lib/fs-atomic.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { mkdir, open, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises'
|
|
3
|
-
import { dirname } from 'node:path'
|
|
4
|
-
|
|
5
|
-
/** Write-tmp-then-rename so readers never observe a partial file. Mode 0600. */
|
|
6
|
-
export async function atomicWrite(path: string, data: string): Promise<void> {
|
|
7
|
-
const tmp = `${path}.${randomUUID()}.tmp`
|
|
8
|
-
await writeFile(tmp, data, { mode: 0o600 })
|
|
9
|
-
await rename(tmp, path)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export type FileLockOptions = {
|
|
13
|
-
/** Delay between acquisition attempts while another process holds the lock. */
|
|
14
|
-
pollIntervalMs?: number
|
|
15
|
-
/** A lock file whose mtime is older than this is considered abandoned. */
|
|
16
|
-
staleAfterMs?: number
|
|
17
|
-
/** Give up acquiring after this long. */
|
|
18
|
-
timeoutMs?: number
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Run `fn` under a cross-process mutex backed by a create-exclusive lock file.
|
|
23
|
-
*
|
|
24
|
-
* Contenders poll until the holder releases (unlinks) the lock. Abandoned
|
|
25
|
-
* locks are taken over when the holder pid is dead or the file's mtime
|
|
26
|
-
* exceeds `staleAfterMs` — a crash mid-section can therefore delay other
|
|
27
|
-
* processes by up to `staleAfterMs`, never deadlock them.
|
|
28
|
-
*/
|
|
29
|
-
export async function withFileLock<T>(
|
|
30
|
-
lockPath: string,
|
|
31
|
-
fn: () => Promise<T>,
|
|
32
|
-
opts: FileLockOptions = {},
|
|
33
|
-
): Promise<T> {
|
|
34
|
-
const pollIntervalMs = opts.pollIntervalMs ?? 100
|
|
35
|
-
const staleAfterMs = opts.staleAfterMs ?? 30_000
|
|
36
|
-
const timeoutMs = opts.timeoutMs ?? 60_000
|
|
37
|
-
const deadline = Date.now() + timeoutMs
|
|
38
|
-
|
|
39
|
-
for (;;) {
|
|
40
|
-
const acquired = await tryAcquire(lockPath)
|
|
41
|
-
if (acquired) {
|
|
42
|
-
try {
|
|
43
|
-
return await fn()
|
|
44
|
-
} finally {
|
|
45
|
-
await unlink(lockPath).catch(() => undefined)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (await takeoverIfAbandoned(lockPath, staleAfterMs)) continue
|
|
49
|
-
if (Date.now() >= deadline) {
|
|
50
|
-
throw new Error(`Timed out after ${timeoutMs}ms waiting for lock ${lockPath}`)
|
|
51
|
-
}
|
|
52
|
-
await sleep(pollIntervalMs)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function tryAcquire(lockPath: string): Promise<boolean> {
|
|
57
|
-
try {
|
|
58
|
-
const handle = await open(lockPath, 'wx', 0o600)
|
|
59
|
-
try {
|
|
60
|
-
await handle.writeFile(
|
|
61
|
-
JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }),
|
|
62
|
-
)
|
|
63
|
-
} finally {
|
|
64
|
-
await handle.close()
|
|
65
|
-
}
|
|
66
|
-
return true
|
|
67
|
-
} catch (e) {
|
|
68
|
-
const code = (e as { code?: string }).code
|
|
69
|
-
if (code === 'EEXIST') return false
|
|
70
|
-
if (code === 'ENOENT') {
|
|
71
|
-
await mkdir(dirname(lockPath), { recursive: true })
|
|
72
|
-
return tryAcquire(lockPath)
|
|
73
|
-
}
|
|
74
|
-
throw e
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Remove the lock if its holder is gone (dead pid, fast) or it outlived
|
|
80
|
-
* `staleAfterMs` (mtime, covers unreadable pids and cross-container holders).
|
|
81
|
-
* Returns true when the caller should immediately retry acquisition —
|
|
82
|
-
* `open('wx')` re-arbitrates between concurrent takeover candidates.
|
|
83
|
-
*/
|
|
84
|
-
async function takeoverIfAbandoned(lockPath: string, staleAfterMs: number): Promise<boolean> {
|
|
85
|
-
try {
|
|
86
|
-
const info = await stat(lockPath)
|
|
87
|
-
if (Date.now() - info.mtimeMs > staleAfterMs) {
|
|
88
|
-
await unlink(lockPath).catch(() => undefined)
|
|
89
|
-
return true
|
|
90
|
-
}
|
|
91
|
-
} catch (e) {
|
|
92
|
-
// Lock vanished between attempts: holder released — retry right away.
|
|
93
|
-
if ((e as { code?: string }).code === 'ENOENT') return true
|
|
94
|
-
throw e
|
|
95
|
-
}
|
|
96
|
-
const pid = await readLockPid(lockPath)
|
|
97
|
-
if (pid !== undefined && pid !== process.pid && !isPidAlive(pid)) {
|
|
98
|
-
await unlink(lockPath).catch(() => undefined)
|
|
99
|
-
return true
|
|
100
|
-
}
|
|
101
|
-
return false
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function readLockPid(lockPath: string): Promise<number | undefined> {
|
|
105
|
-
try {
|
|
106
|
-
const raw = await readFile(lockPath, 'utf-8')
|
|
107
|
-
const pid = (JSON.parse(raw) as { pid?: unknown }).pid
|
|
108
|
-
return typeof pid === 'number' ? pid : undefined
|
|
109
|
-
} catch {
|
|
110
|
-
return undefined
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function isPidAlive(pid: number): boolean {
|
|
115
|
-
try {
|
|
116
|
-
process.kill(pid, 0)
|
|
117
|
-
return true
|
|
118
|
-
} catch (e) {
|
|
119
|
-
// EPERM means the pid exists but belongs to another user — still alive.
|
|
120
|
-
return (e as { code?: string }).code === 'EPERM'
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function sleep(ms: number): Promise<void> {
|
|
125
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
126
|
-
}
|
package/src/lib/identity.ts
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
2
|
-
import { dirname } from 'node:path'
|
|
3
|
-
import { z } from 'zod'
|
|
4
|
-
|
|
5
|
-
import { deleteIdpSession } from './idp'
|
|
6
|
-
import { persistKeypair, removeKeypair } from './keys'
|
|
7
|
-
import { log } from './log'
|
|
8
|
-
import { IDENTITIES_PATH } from './paths'
|
|
9
|
-
import { RegistryModeSchema, validateName, type RegistryMode } from './validation'
|
|
10
|
-
|
|
11
|
-
export const RegistrationSchema = z.object({
|
|
12
|
-
iss: z.string(),
|
|
13
|
-
sub: z.string(),
|
|
14
|
-
registeredAt: z.string(),
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
export const IdentitySchema = z.object({
|
|
18
|
-
subject: z.string(),
|
|
19
|
-
createdAt: z.string(),
|
|
20
|
-
/** `key` identities sign local JWTs; `idp` identities reuse OAuth/OIDC access tokens. */
|
|
21
|
-
source: z.enum(['key', 'idp']).optional(),
|
|
22
|
-
// `local` = machine-only, `remote` = mirrored via astrale cloud (§2.7).
|
|
23
|
-
mode: RegistryModeSchema.optional(),
|
|
24
|
-
/** JWK thumbprint of the identity keypair. Optional for legacy entries. */
|
|
25
|
-
kid: z.string().optional(),
|
|
26
|
-
/** IdP registry name for source=idp identities. */
|
|
27
|
-
idp: z.string().optional(),
|
|
28
|
-
/** OIDC issuer for source=idp identities. */
|
|
29
|
-
issuer: z.string().url().optional(),
|
|
30
|
-
/** Optional preferred audience used during IdP login/refresh flows. */
|
|
31
|
-
audience: z.string().optional(),
|
|
32
|
-
/** Non-secret claims snapshot from the last successful IdP login. */
|
|
33
|
-
claims: z.record(z.string(), z.unknown()).optional(),
|
|
34
|
-
/**
|
|
35
|
-
* Cache of `(iss, sub)` pairs returned by `Identity::registerIdentity`,
|
|
36
|
-
* keyed by instance slug. Populated by `astrale identity register`; consulted
|
|
37
|
-
* by `resolveCredential` so `--as <name> -i <instance>` signs JWTs the kernel
|
|
38
|
-
* accepts.
|
|
39
|
-
*/
|
|
40
|
-
registrations: z.record(z.string(), RegistrationSchema).optional(),
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
export type Registration = z.infer<typeof RegistrationSchema>
|
|
44
|
-
|
|
45
|
-
export const IdentityStoreSchema = z.object({
|
|
46
|
-
default: z.string(),
|
|
47
|
-
identities: z.record(z.string(), IdentitySchema),
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
export type Identity = z.infer<typeof IdentitySchema>
|
|
51
|
-
export type IdentityStore = z.infer<typeof IdentityStoreSchema>
|
|
52
|
-
|
|
53
|
-
function seed(): IdentityStore {
|
|
54
|
-
return {
|
|
55
|
-
default: 'manager',
|
|
56
|
-
identities: {
|
|
57
|
-
manager: {
|
|
58
|
-
subject: 'manager',
|
|
59
|
-
createdAt: new Date().toISOString(),
|
|
60
|
-
source: 'key',
|
|
61
|
-
mode: 'local',
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function readIdentities(): Promise<IdentityStore> {
|
|
68
|
-
try {
|
|
69
|
-
const raw = await readFile(IDENTITIES_PATH, 'utf-8')
|
|
70
|
-
return IdentityStoreSchema.parse(JSON.parse(raw))
|
|
71
|
-
} catch (e) {
|
|
72
|
-
if (e instanceof z.ZodError) {
|
|
73
|
-
log.warn(`Invalid identities at ${IDENTITIES_PATH} — using defaults`)
|
|
74
|
-
} else if ((e as { code?: string }).code !== 'ENOENT') {
|
|
75
|
-
// Present-but-unreadable file (malformed JSON → SyntaxError, or a
|
|
76
|
-
// non-missing-file read error). ENOENT is the legitimate first-run
|
|
77
|
-
// case and stays silent; anything else would otherwise be discarded
|
|
78
|
-
// and could be overwritten by a later writeIdentities.
|
|
79
|
-
log.warn(`Could not read identities at ${IDENTITIES_PATH} — using defaults`)
|
|
80
|
-
}
|
|
81
|
-
return seed()
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function writeIdentities(store: IdentityStore): Promise<void> {
|
|
86
|
-
await mkdir(dirname(IDENTITIES_PATH), { recursive: true })
|
|
87
|
-
await writeFile(IDENTITIES_PATH, JSON.stringify(store, null, 2) + '\n')
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export async function createIdentity(
|
|
91
|
-
name: string,
|
|
92
|
-
opts: {
|
|
93
|
-
subject?: string
|
|
94
|
-
mode?: RegistryMode
|
|
95
|
-
issuer?: string
|
|
96
|
-
kid?: string
|
|
97
|
-
skipKeygen?: boolean
|
|
98
|
-
} = {},
|
|
99
|
-
): Promise<Identity> {
|
|
100
|
-
validateName(name, 'Identity')
|
|
101
|
-
const store = await readIdentities()
|
|
102
|
-
if (store.identities[name]) {
|
|
103
|
-
throw new Error(`Identity "${name}" already exists`)
|
|
104
|
-
}
|
|
105
|
-
const subject = opts.subject ?? name
|
|
106
|
-
let kid: string | undefined
|
|
107
|
-
if (!opts.skipKeygen) {
|
|
108
|
-
const result = await persistKeypair(subject)
|
|
109
|
-
kid = result.kid
|
|
110
|
-
}
|
|
111
|
-
const identity: Identity = {
|
|
112
|
-
subject,
|
|
113
|
-
createdAt: new Date().toISOString(),
|
|
114
|
-
source: 'key',
|
|
115
|
-
mode: opts.mode ?? 'local',
|
|
116
|
-
kid: opts.kid ?? kid,
|
|
117
|
-
issuer: opts.issuer,
|
|
118
|
-
}
|
|
119
|
-
store.identities[name] = identity
|
|
120
|
-
await writeIdentities(store)
|
|
121
|
-
return identity
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export async function upsertKeyIdentity(
|
|
125
|
-
name: string,
|
|
126
|
-
opts: {
|
|
127
|
-
subject: string
|
|
128
|
-
mode?: RegistryMode
|
|
129
|
-
issuer?: string
|
|
130
|
-
kid?: string
|
|
131
|
-
},
|
|
132
|
-
): Promise<Identity> {
|
|
133
|
-
validateName(name, 'Identity')
|
|
134
|
-
const store = await readIdentities()
|
|
135
|
-
const existing = store.identities[name]
|
|
136
|
-
if (existing && (existing.source ?? 'key') !== 'key') {
|
|
137
|
-
throw new Error(`Identity "${name}" already exists and is IdP-backed`)
|
|
138
|
-
}
|
|
139
|
-
const identity: Identity = {
|
|
140
|
-
subject: opts.subject,
|
|
141
|
-
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
142
|
-
source: 'key',
|
|
143
|
-
mode: opts.mode ?? existing?.mode ?? 'local',
|
|
144
|
-
kid: opts.kid,
|
|
145
|
-
issuer: opts.issuer,
|
|
146
|
-
}
|
|
147
|
-
store.identities[name] = identity
|
|
148
|
-
await writeIdentities(store)
|
|
149
|
-
return identity
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export async function deleteIdentity(name: string): Promise<void> {
|
|
153
|
-
const store = await readIdentities()
|
|
154
|
-
const entry = store.identities[name]
|
|
155
|
-
if (!entry) {
|
|
156
|
-
throw new Error(`Identity "${name}" not found`)
|
|
157
|
-
}
|
|
158
|
-
if (store.default === name) {
|
|
159
|
-
throw new Error(
|
|
160
|
-
`Cannot delete the default identity "${name}". Switch default first with: astrale identity use <other>`,
|
|
161
|
-
)
|
|
162
|
-
}
|
|
163
|
-
if ((entry.source ?? 'key') === 'idp') {
|
|
164
|
-
await deleteIdpSession(name)
|
|
165
|
-
} else {
|
|
166
|
-
await removeKeypair(entry.subject)
|
|
167
|
-
}
|
|
168
|
-
delete store.identities[name]
|
|
169
|
-
await writeIdentities(store)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export async function setDefault(name: string): Promise<void> {
|
|
173
|
-
const store = await readIdentities()
|
|
174
|
-
if (!store.identities[name]) {
|
|
175
|
-
throw new Error(`Identity "${name}" not found`)
|
|
176
|
-
}
|
|
177
|
-
store.default = name
|
|
178
|
-
await writeIdentities(store)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export async function getDefault(): Promise<Identity & { name: string }> {
|
|
182
|
-
const store = await readIdentities()
|
|
183
|
-
const identity = store.identities[store.default]
|
|
184
|
-
if (!identity) {
|
|
185
|
-
throw new Error(
|
|
186
|
-
`Default identity "${store.default}" not found. Run: astrale identity create ${store.default}`,
|
|
187
|
-
)
|
|
188
|
-
}
|
|
189
|
-
return { ...identity, name: store.default }
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export async function getIdentity(name: string): Promise<Identity> {
|
|
193
|
-
const store = await readIdentities()
|
|
194
|
-
const identity = store.identities[name]
|
|
195
|
-
if (!identity) {
|
|
196
|
-
throw new Error(`Identity "${name}" not found. Run: astrale identity create ${name}`)
|
|
197
|
-
}
|
|
198
|
-
return identity
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export async function upsertIdpIdentity(
|
|
202
|
-
name: string,
|
|
203
|
-
opts: {
|
|
204
|
-
subject: string
|
|
205
|
-
idp: string
|
|
206
|
-
issuer: string
|
|
207
|
-
audience?: string
|
|
208
|
-
claims?: Record<string, unknown>
|
|
209
|
-
use?: boolean
|
|
210
|
-
},
|
|
211
|
-
): Promise<Identity> {
|
|
212
|
-
validateName(name, 'Identity')
|
|
213
|
-
validateName(opts.idp, 'IdP')
|
|
214
|
-
const store = await readIdentities()
|
|
215
|
-
const existing = store.identities[name]
|
|
216
|
-
if (existing && (existing.source ?? 'key') !== 'idp') {
|
|
217
|
-
throw new Error(`Identity "${name}" already exists and is key-backed`)
|
|
218
|
-
}
|
|
219
|
-
const identity: Identity = {
|
|
220
|
-
subject: opts.subject,
|
|
221
|
-
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
222
|
-
source: 'idp',
|
|
223
|
-
mode: 'remote',
|
|
224
|
-
idp: opts.idp,
|
|
225
|
-
issuer: opts.issuer,
|
|
226
|
-
audience: opts.audience,
|
|
227
|
-
claims: opts.claims,
|
|
228
|
-
registrations: existing?.registrations,
|
|
229
|
-
}
|
|
230
|
-
store.identities[name] = identity
|
|
231
|
-
if (opts.use !== false) store.default = name
|
|
232
|
-
await writeIdentities(store)
|
|
233
|
-
return identity
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** Record (or replace) the kernel-derived (iss, sub) pair for an identity on a target instance. */
|
|
237
|
-
export async function setRegistration(
|
|
238
|
-
name: string,
|
|
239
|
-
instanceSlug: string,
|
|
240
|
-
registration: Registration,
|
|
241
|
-
): Promise<void> {
|
|
242
|
-
const store = await readIdentities()
|
|
243
|
-
const entry = store.identities[name]
|
|
244
|
-
if (!entry) throw new Error(`Identity "${name}" not found`)
|
|
245
|
-
entry.registrations = { ...entry.registrations, [instanceSlug]: registration }
|
|
246
|
-
await writeIdentities(store)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** Migrate an identity to a new registry mode (local ↔ remote, §2.7). */
|
|
250
|
-
export async function setIdentityMode(name: string, mode: RegistryMode): Promise<void> {
|
|
251
|
-
const store = await readIdentities()
|
|
252
|
-
const entry = store.identities[name]
|
|
253
|
-
if (!entry) throw new Error(`Identity "${name}" not found`)
|
|
254
|
-
entry.mode = mode
|
|
255
|
-
await writeIdentities(store)
|
|
256
|
-
}
|