@stoneforge/quarry 0.1.0
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/LICENSE +13 -0
- package/README.md +160 -0
- package/dist/api/index.d.ts +8 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +8 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/quarry-api.d.ts +268 -0
- package/dist/api/quarry-api.d.ts.map +1 -0
- package/dist/api/quarry-api.js +3905 -0
- package/dist/api/quarry-api.js.map +1 -0
- package/dist/api/types.d.ts +1359 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +204 -0
- package/dist/api/types.js.map +1 -0
- package/dist/bin/sf.d.ts +3 -0
- package/dist/bin/sf.d.ts.map +1 -0
- package/dist/bin/sf.js +9 -0
- package/dist/bin/sf.js.map +1 -0
- package/dist/cli/commands/admin.d.ts +11 -0
- package/dist/cli/commands/admin.d.ts.map +1 -0
- package/dist/cli/commands/admin.js +465 -0
- package/dist/cli/commands/admin.js.map +1 -0
- package/dist/cli/commands/alias.d.ts +8 -0
- package/dist/cli/commands/alias.d.ts.map +1 -0
- package/dist/cli/commands/alias.js +70 -0
- package/dist/cli/commands/alias.js.map +1 -0
- package/dist/cli/commands/channel.d.ts +13 -0
- package/dist/cli/commands/channel.d.ts.map +1 -0
- package/dist/cli/commands/channel.js +680 -0
- package/dist/cli/commands/channel.js.map +1 -0
- package/dist/cli/commands/completion.d.ts +8 -0
- package/dist/cli/commands/completion.d.ts.map +1 -0
- package/dist/cli/commands/completion.js +87 -0
- package/dist/cli/commands/completion.js.map +1 -0
- package/dist/cli/commands/config.d.ts +12 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +242 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/crud.d.ts +64 -0
- package/dist/cli/commands/crud.d.ts.map +1 -0
- package/dist/cli/commands/crud.js +805 -0
- package/dist/cli/commands/crud.js.map +1 -0
- package/dist/cli/commands/dep.d.ts +16 -0
- package/dist/cli/commands/dep.d.ts.map +1 -0
- package/dist/cli/commands/dep.js +499 -0
- package/dist/cli/commands/dep.js.map +1 -0
- package/dist/cli/commands/document.d.ts +12 -0
- package/dist/cli/commands/document.d.ts.map +1 -0
- package/dist/cli/commands/document.js +1039 -0
- package/dist/cli/commands/document.js.map +1 -0
- package/dist/cli/commands/embeddings.d.ts +12 -0
- package/dist/cli/commands/embeddings.d.ts.map +1 -0
- package/dist/cli/commands/embeddings.js +273 -0
- package/dist/cli/commands/embeddings.js.map +1 -0
- package/dist/cli/commands/entity.d.ts +16 -0
- package/dist/cli/commands/entity.d.ts.map +1 -0
- package/dist/cli/commands/entity.js +522 -0
- package/dist/cli/commands/entity.js.map +1 -0
- package/dist/cli/commands/gc.d.ts +10 -0
- package/dist/cli/commands/gc.d.ts.map +1 -0
- package/dist/cli/commands/gc.js +257 -0
- package/dist/cli/commands/gc.js.map +1 -0
- package/dist/cli/commands/help.d.ts +11 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/help.js +169 -0
- package/dist/cli/commands/help.js.map +1 -0
- package/dist/cli/commands/history.d.ts +9 -0
- package/dist/cli/commands/history.d.ts.map +1 -0
- package/dist/cli/commands/history.js +160 -0
- package/dist/cli/commands/history.js.map +1 -0
- package/dist/cli/commands/identity.d.ts +18 -0
- package/dist/cli/commands/identity.d.ts.map +1 -0
- package/dist/cli/commands/identity.js +698 -0
- package/dist/cli/commands/identity.js.map +1 -0
- package/dist/cli/commands/inbox.d.ts +20 -0
- package/dist/cli/commands/inbox.d.ts.map +1 -0
- package/dist/cli/commands/inbox.js +493 -0
- package/dist/cli/commands/inbox.js.map +1 -0
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +144 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/install.d.ts +9 -0
- package/dist/cli/commands/install.d.ts.map +1 -0
- package/dist/cli/commands/install.js +200 -0
- package/dist/cli/commands/install.js.map +1 -0
- package/dist/cli/commands/library.d.ts +12 -0
- package/dist/cli/commands/library.d.ts.map +1 -0
- package/dist/cli/commands/library.js +665 -0
- package/dist/cli/commands/library.js.map +1 -0
- package/dist/cli/commands/message.d.ts +11 -0
- package/dist/cli/commands/message.d.ts.map +1 -0
- package/dist/cli/commands/message.js +608 -0
- package/dist/cli/commands/message.js.map +1 -0
- package/dist/cli/commands/plan.d.ts +17 -0
- package/dist/cli/commands/plan.d.ts.map +1 -0
- package/dist/cli/commands/plan.js +698 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/playbook.d.ts +12 -0
- package/dist/cli/commands/playbook.d.ts.map +1 -0
- package/dist/cli/commands/playbook.js +730 -0
- package/dist/cli/commands/playbook.js.map +1 -0
- package/dist/cli/commands/reset.d.ts +12 -0
- package/dist/cli/commands/reset.d.ts.map +1 -0
- package/dist/cli/commands/reset.js +306 -0
- package/dist/cli/commands/reset.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +11 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +106 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +8 -0
- package/dist/cli/commands/stats.d.ts.map +1 -0
- package/dist/cli/commands/stats.js +82 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +14 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +370 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/task.d.ts +25 -0
- package/dist/cli/commands/task.d.ts.map +1 -0
- package/dist/cli/commands/task.js +1153 -0
- package/dist/cli/commands/task.js.map +1 -0
- package/dist/cli/commands/team.d.ts +13 -0
- package/dist/cli/commands/team.d.ts.map +1 -0
- package/dist/cli/commands/team.js +471 -0
- package/dist/cli/commands/team.js.map +1 -0
- package/dist/cli/commands/workflow.d.ts +16 -0
- package/dist/cli/commands/workflow.d.ts.map +1 -0
- package/dist/cli/commands/workflow.js +753 -0
- package/dist/cli/commands/workflow.js.map +1 -0
- package/dist/cli/completion.d.ts +28 -0
- package/dist/cli/completion.d.ts.map +1 -0
- package/dist/cli/completion.js +295 -0
- package/dist/cli/completion.js.map +1 -0
- package/dist/cli/db.d.ts +38 -0
- package/dist/cli/db.d.ts.map +1 -0
- package/dist/cli/db.js +90 -0
- package/dist/cli/db.js.map +1 -0
- package/dist/cli/formatter.d.ts +87 -0
- package/dist/cli/formatter.d.ts.map +1 -0
- package/dist/cli/formatter.js +464 -0
- package/dist/cli/formatter.js.map +1 -0
- package/dist/cli/index.d.ts +33 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +38 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/parser.d.ts +45 -0
- package/dist/cli/parser.d.ts.map +1 -0
- package/dist/cli/parser.js +256 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/cli/plugin-loader.d.ts +39 -0
- package/dist/cli/plugin-loader.d.ts.map +1 -0
- package/dist/cli/plugin-loader.js +165 -0
- package/dist/cli/plugin-loader.js.map +1 -0
- package/dist/cli/plugin-registry.d.ts +50 -0
- package/dist/cli/plugin-registry.d.ts.map +1 -0
- package/dist/cli/plugin-registry.js +206 -0
- package/dist/cli/plugin-registry.js.map +1 -0
- package/dist/cli/plugin-types.d.ts +106 -0
- package/dist/cli/plugin-types.d.ts.map +1 -0
- package/dist/cli/plugin-types.js +103 -0
- package/dist/cli/plugin-types.js.map +1 -0
- package/dist/cli/runner.d.ts +35 -0
- package/dist/cli/runner.d.ts.map +1 -0
- package/dist/cli/runner.js +340 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli/suggest.d.ts +15 -0
- package/dist/cli/suggest.d.ts.map +1 -0
- package/dist/cli/suggest.js +49 -0
- package/dist/cli/suggest.js.map +1 -0
- package/dist/cli/types.d.ts +138 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +63 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/config/config.d.ts +86 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +348 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/defaults.d.ts +66 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +114 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/duration.d.ts +75 -0
- package/dist/config/duration.d.ts.map +1 -0
- package/dist/config/duration.js +190 -0
- package/dist/config/duration.js.map +1 -0
- package/dist/config/env.d.ts +67 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +207 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/file.d.ts +97 -0
- package/dist/config/file.d.ts.map +1 -0
- package/dist/config/file.js +365 -0
- package/dist/config/file.js.map +1 -0
- package/dist/config/index.d.ts +35 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +41 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/merge.d.ts +53 -0
- package/dist/config/merge.d.ts.map +1 -0
- package/dist/config/merge.js +226 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/config/types.d.ts +257 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +72 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/validation.d.ts +55 -0
- package/dist/config/validation.d.ts.map +1 -0
- package/dist/config/validation.js +251 -0
- package/dist/config/validation.js.map +1 -0
- package/dist/http/index.d.ts +8 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +12 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/sync-handlers.d.ts +162 -0
- package/dist/http/sync-handlers.d.ts.map +1 -0
- package/dist/http/sync-handlers.js +271 -0
- package/dist/http/sync-handlers.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.d.ts +34 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +3329 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/static.d.ts +18 -0
- package/dist/server/static.d.ts.map +1 -0
- package/dist/server/static.js +71 -0
- package/dist/server/static.js.map +1 -0
- package/dist/server/ws/broadcaster.d.ts +8 -0
- package/dist/server/ws/broadcaster.d.ts.map +1 -0
- package/dist/server/ws/broadcaster.js +7 -0
- package/dist/server/ws/broadcaster.js.map +1 -0
- package/dist/server/ws/handler.d.ts +55 -0
- package/dist/server/ws/handler.d.ts.map +1 -0
- package/dist/server/ws/handler.js +160 -0
- package/dist/server/ws/handler.js.map +1 -0
- package/dist/services/blocked-cache.d.ts +297 -0
- package/dist/services/blocked-cache.d.ts.map +1 -0
- package/dist/services/blocked-cache.js +755 -0
- package/dist/services/blocked-cache.js.map +1 -0
- package/dist/services/dependency.d.ts +205 -0
- package/dist/services/dependency.d.ts.map +1 -0
- package/dist/services/dependency.js +566 -0
- package/dist/services/dependency.js.map +1 -0
- package/dist/services/embeddings/fusion.d.ts +33 -0
- package/dist/services/embeddings/fusion.d.ts.map +1 -0
- package/dist/services/embeddings/fusion.js +34 -0
- package/dist/services/embeddings/fusion.js.map +1 -0
- package/dist/services/embeddings/index.d.ts +12 -0
- package/dist/services/embeddings/index.d.ts.map +1 -0
- package/dist/services/embeddings/index.js +10 -0
- package/dist/services/embeddings/index.js.map +1 -0
- package/dist/services/embeddings/local-provider.d.ts +31 -0
- package/dist/services/embeddings/local-provider.d.ts.map +1 -0
- package/dist/services/embeddings/local-provider.js +80 -0
- package/dist/services/embeddings/local-provider.js.map +1 -0
- package/dist/services/embeddings/service.d.ts +76 -0
- package/dist/services/embeddings/service.d.ts.map +1 -0
- package/dist/services/embeddings/service.js +153 -0
- package/dist/services/embeddings/service.js.map +1 -0
- package/dist/services/embeddings/types.d.ts +70 -0
- package/dist/services/embeddings/types.d.ts.map +1 -0
- package/dist/services/embeddings/types.js +8 -0
- package/dist/services/embeddings/types.js.map +1 -0
- package/dist/services/id-length-cache.d.ts +156 -0
- package/dist/services/id-length-cache.d.ts.map +1 -0
- package/dist/services/id-length-cache.js +197 -0
- package/dist/services/id-length-cache.js.map +1 -0
- package/dist/services/inbox.d.ts +147 -0
- package/dist/services/inbox.d.ts.map +1 -0
- package/dist/services/inbox.js +428 -0
- package/dist/services/inbox.js.map +1 -0
- package/dist/services/index.d.ts +10 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +10 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/priority-service.d.ts +145 -0
- package/dist/services/priority-service.d.ts.map +1 -0
- package/dist/services/priority-service.js +272 -0
- package/dist/services/priority-service.js.map +1 -0
- package/dist/services/search-utils.d.ts +47 -0
- package/dist/services/search-utils.d.ts.map +1 -0
- package/dist/services/search-utils.js +83 -0
- package/dist/services/search-utils.js.map +1 -0
- package/dist/sync/hash.d.ts +48 -0
- package/dist/sync/hash.d.ts.map +1 -0
- package/dist/sync/hash.js +136 -0
- package/dist/sync/hash.js.map +1 -0
- package/dist/sync/index.d.ts +11 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +16 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/merge.d.ts +80 -0
- package/dist/sync/merge.d.ts.map +1 -0
- package/dist/sync/merge.js +310 -0
- package/dist/sync/merge.js.map +1 -0
- package/dist/sync/serialization.d.ts +132 -0
- package/dist/sync/serialization.d.ts.map +1 -0
- package/dist/sync/serialization.js +306 -0
- package/dist/sync/serialization.js.map +1 -0
- package/dist/sync/service.d.ts +102 -0
- package/dist/sync/service.d.ts.map +1 -0
- package/dist/sync/service.js +493 -0
- package/dist/sync/service.js.map +1 -0
- package/dist/sync/types.d.ts +275 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +76 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/systems/identity.d.ts +479 -0
- package/dist/systems/identity.d.ts.map +1 -0
- package/dist/systems/identity.js +817 -0
- package/dist/systems/identity.js.map +1 -0
- package/dist/systems/index.d.ts +8 -0
- package/dist/systems/index.d.ts.map +1 -0
- package/dist/systems/index.js +29 -0
- package/dist/systems/index.js.map +1 -0
- package/package.json +121 -0
- package/web/assets/charts-vendor-D1YcbGux.js +55 -0
- package/web/assets/dnd-vendor-DmxE-_ZH.js +5 -0
- package/web/assets/editor-vendor-BxraAWts.js +279 -0
- package/web/assets/index-B77vv208.js +341 -0
- package/web/assets/index-CF_XnVLh.css +1 -0
- package/web/assets/router-vendor-BCKpRBrB.js +41 -0
- package/web/assets/ui-vendor-DUahGnbT.js +45 -0
- package/web/assets/utils-vendor-CfYKiENT.js +813 -0
- package/web/favicon.ico +0 -0
- package/web/index.html +23 -0
- package/web/logo.png +0 -0
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity System - Authentication and Signature Verification
|
|
3
|
+
*
|
|
4
|
+
* The identity system manages entity authentication and verification, supporting
|
|
5
|
+
* both soft (name-based) and cryptographic (key-based) identity models.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Identity mode configuration (soft, cryptographic, hybrid)
|
|
9
|
+
* - Ed25519 signature generation and verification
|
|
10
|
+
* - Signed request validation with time tolerance
|
|
11
|
+
* - Actor context management
|
|
12
|
+
*/
|
|
13
|
+
import { IdentityError, ValidationError, ErrorCode, isValidTimestamp } from '@stoneforge/core';
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Identity Mode Types
|
|
16
|
+
// ============================================================================
|
|
17
|
+
/**
|
|
18
|
+
* Identity mode determines the level of verification required
|
|
19
|
+
*/
|
|
20
|
+
export const IdentityMode = {
|
|
21
|
+
/** Name-based identity without verification (default) */
|
|
22
|
+
SOFT: 'soft',
|
|
23
|
+
/** Key-based identity with signature verification */
|
|
24
|
+
CRYPTOGRAPHIC: 'cryptographic',
|
|
25
|
+
/** Mixed mode - accepts both verified and unverified actors */
|
|
26
|
+
HYBRID: 'hybrid',
|
|
27
|
+
};
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Verification Types
|
|
30
|
+
// ============================================================================
|
|
31
|
+
/**
|
|
32
|
+
* Result of signature verification
|
|
33
|
+
*/
|
|
34
|
+
export const VerificationStatus = {
|
|
35
|
+
/** Signature is valid */
|
|
36
|
+
VALID: 'valid',
|
|
37
|
+
/** Signature is invalid or doesn't match */
|
|
38
|
+
INVALID: 'invalid',
|
|
39
|
+
/** Signature has expired (outside time tolerance) */
|
|
40
|
+
EXPIRED: 'expired',
|
|
41
|
+
/** Entity not found for verification */
|
|
42
|
+
ACTOR_NOT_FOUND: 'actor_not_found',
|
|
43
|
+
/** Entity has no public key */
|
|
44
|
+
NO_PUBLIC_KEY: 'no_public_key',
|
|
45
|
+
/** Signature was not provided */
|
|
46
|
+
NOT_SIGNED: 'not_signed',
|
|
47
|
+
};
|
|
48
|
+
/** Default time tolerance: 5 minutes in milliseconds */
|
|
49
|
+
export const DEFAULT_TIME_TOLERANCE = 5 * 60 * 1000;
|
|
50
|
+
/**
|
|
51
|
+
* Default identity configuration
|
|
52
|
+
*/
|
|
53
|
+
export const DEFAULT_IDENTITY_CONFIG = {
|
|
54
|
+
mode: IdentityMode.SOFT,
|
|
55
|
+
timeTolerance: DEFAULT_TIME_TOLERANCE,
|
|
56
|
+
allowUnregisteredActors: true,
|
|
57
|
+
};
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Validation Constants
|
|
60
|
+
// ============================================================================
|
|
61
|
+
/** Base64 pattern for Ed25519 public keys (44 characters for 32 bytes) */
|
|
62
|
+
const PUBLIC_KEY_PATTERN = /^[A-Za-z0-9+/]{43}=$/;
|
|
63
|
+
/** Base64 pattern for Ed25519 signatures (88 characters for 64 bytes) */
|
|
64
|
+
const SIGNATURE_PATTERN = /^[A-Za-z0-9+/]{86}==$/;
|
|
65
|
+
/** Minimum request hash length (SHA256 hex = 64 characters) */
|
|
66
|
+
const MIN_REQUEST_HASH_LENGTH = 64;
|
|
67
|
+
/** Request hash pattern (hex-encoded SHA256) */
|
|
68
|
+
const REQUEST_HASH_PATTERN = /^[a-f0-9]{64}$/i;
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Validation Functions
|
|
71
|
+
// ============================================================================
|
|
72
|
+
/**
|
|
73
|
+
* Validates an identity mode value
|
|
74
|
+
*/
|
|
75
|
+
export function isValidIdentityMode(value) {
|
|
76
|
+
return (typeof value === 'string' &&
|
|
77
|
+
Object.values(IdentityMode).includes(value));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Validates identity mode and throws if invalid
|
|
81
|
+
*/
|
|
82
|
+
export function validateIdentityMode(value) {
|
|
83
|
+
if (!isValidIdentityMode(value)) {
|
|
84
|
+
throw new ValidationError(`Invalid identity mode: ${value}. Must be one of: ${Object.values(IdentityMode).join(', ')}`, ErrorCode.INVALID_INPUT, { field: 'mode', value, expected: Object.values(IdentityMode) });
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Validates a base64-encoded Ed25519 public key format
|
|
90
|
+
*/
|
|
91
|
+
export function isValidPublicKey(value) {
|
|
92
|
+
if (typeof value !== 'string') {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return PUBLIC_KEY_PATTERN.test(value);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Validates a public key and throws if invalid format
|
|
99
|
+
*/
|
|
100
|
+
export function validatePublicKey(value) {
|
|
101
|
+
if (typeof value !== 'string') {
|
|
102
|
+
throw new IdentityError('Public key must be a string', ErrorCode.INVALID_PUBLIC_KEY, { field: 'publicKey', value, expected: 'string' });
|
|
103
|
+
}
|
|
104
|
+
if (!PUBLIC_KEY_PATTERN.test(value)) {
|
|
105
|
+
throw new IdentityError('Invalid public key format. Expected base64-encoded Ed25519 public key (44 characters)', ErrorCode.INVALID_PUBLIC_KEY, { field: 'publicKey', value, expected: '44-character base64 string ending with =' });
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Validates a base64-encoded Ed25519 signature format
|
|
111
|
+
*/
|
|
112
|
+
export function isValidSignature(value) {
|
|
113
|
+
if (typeof value !== 'string') {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
return SIGNATURE_PATTERN.test(value);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Validates a signature and throws if invalid format
|
|
120
|
+
*/
|
|
121
|
+
export function validateSignature(value) {
|
|
122
|
+
if (typeof value !== 'string') {
|
|
123
|
+
throw new IdentityError('Signature must be a string', ErrorCode.INVALID_SIGNATURE, { field: 'signature', value, expected: 'string' });
|
|
124
|
+
}
|
|
125
|
+
if (!SIGNATURE_PATTERN.test(value)) {
|
|
126
|
+
throw new IdentityError('Invalid signature format. Expected base64-encoded Ed25519 signature (88 characters)', ErrorCode.INVALID_SIGNATURE, { field: 'signature', value, expected: '88-character base64 string ending with ==' });
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Validates a request hash (SHA256 hex)
|
|
132
|
+
*/
|
|
133
|
+
export function isValidRequestHash(value) {
|
|
134
|
+
if (typeof value !== 'string') {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (value.length < MIN_REQUEST_HASH_LENGTH) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return REQUEST_HASH_PATTERN.test(value);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Validates a request hash and throws if invalid
|
|
144
|
+
*/
|
|
145
|
+
export function validateRequestHash(value) {
|
|
146
|
+
if (typeof value !== 'string') {
|
|
147
|
+
throw new ValidationError('Request hash must be a string', ErrorCode.INVALID_INPUT, { field: 'requestHash', value, expected: 'string' });
|
|
148
|
+
}
|
|
149
|
+
if (!REQUEST_HASH_PATTERN.test(value)) {
|
|
150
|
+
throw new ValidationError('Invalid request hash format. Expected 64-character hex-encoded SHA256 hash', ErrorCode.INVALID_INPUT, { field: 'requestHash', value, expected: '64-character hex string' });
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Validates time tolerance value
|
|
156
|
+
*/
|
|
157
|
+
export function isValidTimeTolerance(value) {
|
|
158
|
+
return (typeof value === 'number' &&
|
|
159
|
+
Number.isFinite(value) &&
|
|
160
|
+
value > 0 &&
|
|
161
|
+
value <= 24 * 60 * 60 * 1000 // Max 24 hours
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Validates time tolerance and throws if invalid
|
|
166
|
+
*/
|
|
167
|
+
export function validateTimeTolerance(value) {
|
|
168
|
+
if (!isValidTimeTolerance(value)) {
|
|
169
|
+
throw new ValidationError('Time tolerance must be a positive number (max 24 hours in milliseconds)', ErrorCode.INVALID_INPUT, { field: 'timeTolerance', value, expected: 'positive number <= 86400000' });
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// Type Guards
|
|
175
|
+
// ============================================================================
|
|
176
|
+
/**
|
|
177
|
+
* Type guard for SignedRequestFields
|
|
178
|
+
*/
|
|
179
|
+
export function isSignedRequestFields(value) {
|
|
180
|
+
if (typeof value !== 'object' || value === null) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const obj = value;
|
|
184
|
+
return (isValidSignature(obj.signature) &&
|
|
185
|
+
isValidTimestamp(obj.signedAt) &&
|
|
186
|
+
typeof obj.actor === 'string' &&
|
|
187
|
+
obj.actor.length > 0);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Validates SignedRequestFields and throws detailed errors
|
|
191
|
+
*/
|
|
192
|
+
export function validateSignedRequestFields(value) {
|
|
193
|
+
if (typeof value !== 'object' || value === null) {
|
|
194
|
+
throw new ValidationError('Signed request fields must be an object', ErrorCode.INVALID_INPUT, { value });
|
|
195
|
+
}
|
|
196
|
+
const obj = value;
|
|
197
|
+
// Validate signature
|
|
198
|
+
validateSignature(obj.signature);
|
|
199
|
+
// Validate signedAt
|
|
200
|
+
if (!isValidTimestamp(obj.signedAt)) {
|
|
201
|
+
throw new ValidationError('signedAt must be a valid ISO 8601 timestamp', ErrorCode.INVALID_TIMESTAMP, { field: 'signedAt', value: obj.signedAt });
|
|
202
|
+
}
|
|
203
|
+
// Validate actor
|
|
204
|
+
if (typeof obj.actor !== 'string' || obj.actor.length === 0) {
|
|
205
|
+
throw new ValidationError('Actor must be a non-empty string', ErrorCode.INVALID_INPUT, { field: 'actor', value: obj.actor });
|
|
206
|
+
}
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Type guard for VerificationResult
|
|
211
|
+
*/
|
|
212
|
+
export function isVerificationResult(value) {
|
|
213
|
+
if (typeof value !== 'object' || value === null) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
const obj = value;
|
|
217
|
+
return (typeof obj.status === 'string' &&
|
|
218
|
+
Object.values(VerificationStatus).includes(obj.status) &&
|
|
219
|
+
typeof obj.allowed === 'boolean');
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Type guard for IdentityConfig
|
|
223
|
+
*/
|
|
224
|
+
export function isIdentityConfig(value) {
|
|
225
|
+
if (typeof value !== 'object' || value === null) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
const obj = value;
|
|
229
|
+
return (isValidIdentityMode(obj.mode) &&
|
|
230
|
+
isValidTimeTolerance(obj.timeTolerance) &&
|
|
231
|
+
typeof obj.allowUnregisteredActors === 'boolean');
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Validates IdentityConfig and throws detailed errors
|
|
235
|
+
*/
|
|
236
|
+
export function validateIdentityConfig(value) {
|
|
237
|
+
if (typeof value !== 'object' || value === null) {
|
|
238
|
+
throw new ValidationError('Identity config must be an object', ErrorCode.INVALID_INPUT, { value });
|
|
239
|
+
}
|
|
240
|
+
const obj = value;
|
|
241
|
+
validateIdentityMode(obj.mode);
|
|
242
|
+
validateTimeTolerance(obj.timeTolerance);
|
|
243
|
+
if (typeof obj.allowUnregisteredActors !== 'boolean') {
|
|
244
|
+
throw new ValidationError('allowUnregisteredActors must be a boolean', ErrorCode.INVALID_INPUT, { field: 'allowUnregisteredActors', value: obj.allowUnregisteredActors });
|
|
245
|
+
}
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
// ============================================================================
|
|
249
|
+
// Signed Data Construction
|
|
250
|
+
// ============================================================================
|
|
251
|
+
/**
|
|
252
|
+
* Constructs the signed data string from components
|
|
253
|
+
* Format: actor|signedAt|requestHash
|
|
254
|
+
*/
|
|
255
|
+
export function constructSignedData(data) {
|
|
256
|
+
return `${data.actor}|${data.signedAt}|${data.requestHash}`;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Parses a signed data string into components
|
|
260
|
+
* Format: actor|signedAt|requestHash
|
|
261
|
+
*/
|
|
262
|
+
export function parseSignedData(signedDataString) {
|
|
263
|
+
const parts = signedDataString.split('|');
|
|
264
|
+
if (parts.length !== 3) {
|
|
265
|
+
throw new ValidationError('Invalid signed data format. Expected: actor|signedAt|requestHash', ErrorCode.INVALID_INPUT, { value: signedDataString, expected: 'actor|signedAt|requestHash' });
|
|
266
|
+
}
|
|
267
|
+
const [actor, signedAt, requestHash] = parts;
|
|
268
|
+
if (!actor || actor.length === 0) {
|
|
269
|
+
throw new ValidationError('Actor in signed data cannot be empty', ErrorCode.INVALID_INPUT, { field: 'actor', value: actor });
|
|
270
|
+
}
|
|
271
|
+
if (!isValidTimestamp(signedAt)) {
|
|
272
|
+
throw new ValidationError('Invalid signedAt timestamp in signed data', ErrorCode.INVALID_TIMESTAMP, { field: 'signedAt', value: signedAt });
|
|
273
|
+
}
|
|
274
|
+
validateRequestHash(requestHash);
|
|
275
|
+
return {
|
|
276
|
+
actor,
|
|
277
|
+
signedAt: signedAt,
|
|
278
|
+
requestHash,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// ============================================================================
|
|
282
|
+
// Time Tolerance Checking
|
|
283
|
+
// ============================================================================
|
|
284
|
+
/**
|
|
285
|
+
* Checks if a signature timestamp is within the allowed time tolerance
|
|
286
|
+
*
|
|
287
|
+
* @param signedAt - The timestamp when the request was signed
|
|
288
|
+
* @param tolerance - Time tolerance in milliseconds (default: 5 minutes)
|
|
289
|
+
* @param now - Current timestamp for testing (defaults to now)
|
|
290
|
+
* @returns Object with validity and age information
|
|
291
|
+
*/
|
|
292
|
+
export function checkTimeTolerance(signedAt, tolerance = DEFAULT_TIME_TOLERANCE, now) {
|
|
293
|
+
const signedTime = new Date(signedAt).getTime();
|
|
294
|
+
const currentTime = (now ?? new Date()).getTime();
|
|
295
|
+
const ageMs = Math.abs(currentTime - signedTime);
|
|
296
|
+
if (ageMs > tolerance) {
|
|
297
|
+
return {
|
|
298
|
+
valid: false,
|
|
299
|
+
ageMs,
|
|
300
|
+
expiredBy: ageMs - tolerance,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return { valid: true, ageMs };
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Checks time tolerance and throws if expired
|
|
307
|
+
*/
|
|
308
|
+
export function validateTimeTolerance2(signedAt, tolerance = DEFAULT_TIME_TOLERANCE, now) {
|
|
309
|
+
const result = checkTimeTolerance(signedAt, tolerance, now);
|
|
310
|
+
if (!result.valid) {
|
|
311
|
+
throw new IdentityError(`Signature expired. Signed ${Math.round(result.ageMs / 1000)}s ago, tolerance is ${Math.round(tolerance / 1000)}s`, ErrorCode.SIGNATURE_EXPIRED, {
|
|
312
|
+
signedAt,
|
|
313
|
+
ageMs: result.ageMs,
|
|
314
|
+
tolerance,
|
|
315
|
+
expiredBy: result.expiredBy,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// ============================================================================
|
|
320
|
+
// Verification Result Factories
|
|
321
|
+
// ============================================================================
|
|
322
|
+
/**
|
|
323
|
+
* Creates a successful verification result
|
|
324
|
+
*/
|
|
325
|
+
export function verificationSuccess(actor, ageMs) {
|
|
326
|
+
return {
|
|
327
|
+
status: VerificationStatus.VALID,
|
|
328
|
+
allowed: true,
|
|
329
|
+
actor,
|
|
330
|
+
details: ageMs !== undefined ? { signatureAgeMs: ageMs } : undefined,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Creates a failed verification result
|
|
335
|
+
*/
|
|
336
|
+
export function verificationFailure(status, error, details) {
|
|
337
|
+
return {
|
|
338
|
+
status,
|
|
339
|
+
allowed: false,
|
|
340
|
+
error,
|
|
341
|
+
details,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Creates a "not signed" result (may be allowed in soft/hybrid mode)
|
|
346
|
+
*/
|
|
347
|
+
export function verificationNotSigned(allowed, actor) {
|
|
348
|
+
return {
|
|
349
|
+
status: VerificationStatus.NOT_SIGNED,
|
|
350
|
+
allowed,
|
|
351
|
+
actor,
|
|
352
|
+
error: allowed ? undefined : 'Signature required in cryptographic mode',
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
// ============================================================================
|
|
356
|
+
// Ed25519 Cryptographic Operations
|
|
357
|
+
// ============================================================================
|
|
358
|
+
/**
|
|
359
|
+
* Verifies an Ed25519 signature using Bun's native crypto
|
|
360
|
+
*
|
|
361
|
+
* @param publicKey - Base64-encoded Ed25519 public key
|
|
362
|
+
* @param signature - Base64-encoded Ed25519 signature
|
|
363
|
+
* @param data - The data that was signed (as string or Uint8Array)
|
|
364
|
+
* @returns true if signature is valid, false otherwise
|
|
365
|
+
*/
|
|
366
|
+
export async function verifyEd25519Signature(publicKey, signature, data) {
|
|
367
|
+
try {
|
|
368
|
+
// Decode base64 inputs
|
|
369
|
+
const publicKeyBytes = new Uint8Array(Buffer.from(publicKey, 'base64'));
|
|
370
|
+
const signatureBytes = new Uint8Array(Buffer.from(signature, 'base64'));
|
|
371
|
+
// Convert data to Uint8Array
|
|
372
|
+
const dataBytes = typeof data === 'string'
|
|
373
|
+
? new TextEncoder().encode(data)
|
|
374
|
+
: new Uint8Array(data);
|
|
375
|
+
// Validate key and signature lengths
|
|
376
|
+
if (publicKeyBytes.length !== 32) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
if (signatureBytes.length !== 64) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
// Use Web Crypto API for Ed25519 verification
|
|
383
|
+
const cryptoKey = await crypto.subtle.importKey('raw', publicKeyBytes, { name: 'Ed25519' }, false, ['verify']);
|
|
384
|
+
return await crypto.subtle.verify({ name: 'Ed25519' }, cryptoKey, signatureBytes, dataBytes);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// Any crypto error means verification failed
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Signs data using Ed25519 (for testing purposes)
|
|
393
|
+
* In production, signing should be done by the entity externally
|
|
394
|
+
*
|
|
395
|
+
* @param privateKey - Base64-encoded Ed25519 private key in PKCS8 format
|
|
396
|
+
* @param data - The data to sign
|
|
397
|
+
* @returns Base64-encoded signature
|
|
398
|
+
*/
|
|
399
|
+
export async function signEd25519(privateKey, data) {
|
|
400
|
+
const privateKeyBytes = Buffer.from(privateKey, 'base64');
|
|
401
|
+
// Convert data to Uint8Array
|
|
402
|
+
const dataBytes = typeof data === 'string'
|
|
403
|
+
? new TextEncoder().encode(data)
|
|
404
|
+
: new Uint8Array(data);
|
|
405
|
+
// Import the private key in PKCS8 format
|
|
406
|
+
const cryptoKey = await crypto.subtle.importKey('pkcs8', privateKeyBytes, { name: 'Ed25519' }, false, ['sign']);
|
|
407
|
+
// Sign the data
|
|
408
|
+
const signatureBytes = await crypto.subtle.sign({ name: 'Ed25519' }, cryptoKey, dataBytes);
|
|
409
|
+
// Return base64-encoded signature
|
|
410
|
+
return Buffer.from(signatureBytes).toString('base64');
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Generates a new Ed25519 keypair (for testing purposes)
|
|
414
|
+
*
|
|
415
|
+
* @returns Object with base64-encoded public and private keys (PKCS8 format for private)
|
|
416
|
+
*/
|
|
417
|
+
export async function generateEd25519Keypair() {
|
|
418
|
+
const keypair = await crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']);
|
|
419
|
+
// Export public key as raw bytes (32 bytes -> 44 base64 chars)
|
|
420
|
+
const publicKeyBuffer = await crypto.subtle.exportKey('raw', keypair.publicKey);
|
|
421
|
+
// Export private key in PKCS8 format (48 bytes -> 64 base64 chars)
|
|
422
|
+
const privateKeyBuffer = await crypto.subtle.exportKey('pkcs8', keypair.privateKey);
|
|
423
|
+
return {
|
|
424
|
+
publicKey: Buffer.from(new Uint8Array(publicKeyBuffer)).toString('base64'),
|
|
425
|
+
privateKey: Buffer.from(new Uint8Array(privateKeyBuffer)).toString('base64'),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Performs full signature verification pipeline
|
|
430
|
+
*
|
|
431
|
+
* 1. Validates signature format
|
|
432
|
+
* 2. Looks up entity's public key
|
|
433
|
+
* 3. Constructs signed data
|
|
434
|
+
* 4. Checks time tolerance
|
|
435
|
+
* 5. Verifies signature cryptographically
|
|
436
|
+
*/
|
|
437
|
+
export async function verifySignature(options) {
|
|
438
|
+
const config = { ...DEFAULT_IDENTITY_CONFIG, ...options.config };
|
|
439
|
+
const { signedRequest, requestHash, lookupEntity, now } = options;
|
|
440
|
+
// 1. Validate request hash format
|
|
441
|
+
if (!isValidRequestHash(requestHash)) {
|
|
442
|
+
return verificationFailure(VerificationStatus.INVALID, 'Invalid request hash format');
|
|
443
|
+
}
|
|
444
|
+
// 2. Look up entity
|
|
445
|
+
const entity = await lookupEntity(signedRequest.actor);
|
|
446
|
+
if (!entity) {
|
|
447
|
+
return verificationFailure(VerificationStatus.ACTOR_NOT_FOUND, `Actor '${signedRequest.actor}' not found`, { entityFound: false });
|
|
448
|
+
}
|
|
449
|
+
// 3. Check for public key
|
|
450
|
+
if (!entity.publicKey) {
|
|
451
|
+
return verificationFailure(VerificationStatus.NO_PUBLIC_KEY, `Actor '${signedRequest.actor}' has no public key`, { entityFound: true, hasPublicKey: false });
|
|
452
|
+
}
|
|
453
|
+
// 4. Validate public key format
|
|
454
|
+
if (!isValidPublicKey(entity.publicKey)) {
|
|
455
|
+
return verificationFailure(VerificationStatus.INVALID, 'Entity has invalid public key format', { entityFound: true, hasPublicKey: true });
|
|
456
|
+
}
|
|
457
|
+
// 5. Check time tolerance
|
|
458
|
+
const timeCheck = checkTimeTolerance(signedRequest.signedAt, config.timeTolerance, now);
|
|
459
|
+
if (!timeCheck.valid) {
|
|
460
|
+
return verificationFailure(VerificationStatus.EXPIRED, `Signature expired. Age: ${Math.round(timeCheck.ageMs / 1000)}s, tolerance: ${Math.round(config.timeTolerance / 1000)}s`, { signatureAgeMs: timeCheck.ageMs });
|
|
461
|
+
}
|
|
462
|
+
// 6. Construct signed data and verify
|
|
463
|
+
const signedData = constructSignedData({
|
|
464
|
+
actor: signedRequest.actor,
|
|
465
|
+
signedAt: signedRequest.signedAt,
|
|
466
|
+
requestHash,
|
|
467
|
+
});
|
|
468
|
+
const isValid = await verifyEd25519Signature(entity.publicKey, signedRequest.signature, signedData);
|
|
469
|
+
if (!isValid) {
|
|
470
|
+
return verificationFailure(VerificationStatus.INVALID, 'Signature verification failed', { signatureAgeMs: timeCheck.ageMs, entityFound: true, hasPublicKey: true });
|
|
471
|
+
}
|
|
472
|
+
return verificationSuccess(signedRequest.actor, timeCheck.ageMs);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Determines if a request should be allowed based on identity mode
|
|
476
|
+
*/
|
|
477
|
+
export function shouldAllowRequest(mode, verificationResult) {
|
|
478
|
+
switch (mode) {
|
|
479
|
+
case IdentityMode.SOFT:
|
|
480
|
+
// Always allow in soft mode
|
|
481
|
+
return true;
|
|
482
|
+
case IdentityMode.CRYPTOGRAPHIC:
|
|
483
|
+
// Only allow valid signatures
|
|
484
|
+
return verificationResult.status === VerificationStatus.VALID;
|
|
485
|
+
case IdentityMode.HYBRID:
|
|
486
|
+
// Allow valid signatures or unsigned requests
|
|
487
|
+
return (verificationResult.status === VerificationStatus.VALID ||
|
|
488
|
+
verificationResult.status === VerificationStatus.NOT_SIGNED);
|
|
489
|
+
default:
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// ============================================================================
|
|
494
|
+
// Utility Functions
|
|
495
|
+
// ============================================================================
|
|
496
|
+
/**
|
|
497
|
+
* Creates a SHA256 hash of the request body for signing
|
|
498
|
+
*/
|
|
499
|
+
export async function hashRequestBody(body) {
|
|
500
|
+
const data = typeof body === 'string' ? body : JSON.stringify(body);
|
|
501
|
+
const encoder = new TextEncoder();
|
|
502
|
+
const dataBuffer = encoder.encode(data);
|
|
503
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
|
|
504
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
505
|
+
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Creates a signed request from signing input
|
|
509
|
+
*/
|
|
510
|
+
export async function createSignedRequest(input, privateKey, signedAt) {
|
|
511
|
+
const timestamp = signedAt ?? new Date().toISOString();
|
|
512
|
+
const signedData = constructSignedData({
|
|
513
|
+
actor: input.actor,
|
|
514
|
+
signedAt: timestamp,
|
|
515
|
+
requestHash: input.requestHash,
|
|
516
|
+
});
|
|
517
|
+
const signature = await signEd25519(privateKey, signedData);
|
|
518
|
+
return {
|
|
519
|
+
signature,
|
|
520
|
+
signedAt: timestamp,
|
|
521
|
+
actor: input.actor,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Merges partial config with defaults
|
|
526
|
+
*/
|
|
527
|
+
export function createIdentityConfig(partial) {
|
|
528
|
+
return {
|
|
529
|
+
...DEFAULT_IDENTITY_CONFIG,
|
|
530
|
+
...partial,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
// ============================================================================
|
|
534
|
+
// Actor Context Management (Phase 2: Soft Identity)
|
|
535
|
+
// ============================================================================
|
|
536
|
+
/**
|
|
537
|
+
* Sources from which actor identity can be determined
|
|
538
|
+
*/
|
|
539
|
+
export const ActorSource = {
|
|
540
|
+
/** Explicitly provided in the operation */
|
|
541
|
+
EXPLICIT: 'explicit',
|
|
542
|
+
/** From CLI --actor flag */
|
|
543
|
+
CLI_FLAG: 'cli_flag',
|
|
544
|
+
/** From configuration file */
|
|
545
|
+
CONFIG: 'config',
|
|
546
|
+
/** From element's createdBy field (fallback) */
|
|
547
|
+
ELEMENT: 'element',
|
|
548
|
+
/** System-generated operations */
|
|
549
|
+
SYSTEM: 'system',
|
|
550
|
+
};
|
|
551
|
+
/**
|
|
552
|
+
* Resolves actor context from multiple sources
|
|
553
|
+
*
|
|
554
|
+
* Priority order (highest to lowest):
|
|
555
|
+
* 1. Explicit actor (provided in operation)
|
|
556
|
+
* 2. CLI actor (--actor flag)
|
|
557
|
+
* 3. Config actor (default actor in config)
|
|
558
|
+
* 4. Element's createdBy (fallback for updates/deletes)
|
|
559
|
+
*
|
|
560
|
+
* @param options - Resolution options with actor sources
|
|
561
|
+
* @returns The resolved actor context
|
|
562
|
+
* @throws ValidationError if no actor can be resolved
|
|
563
|
+
*/
|
|
564
|
+
export function resolveActor(options) {
|
|
565
|
+
// Try each source in priority order
|
|
566
|
+
if (options.explicitActor) {
|
|
567
|
+
return {
|
|
568
|
+
actor: options.explicitActor,
|
|
569
|
+
source: ActorSource.EXPLICIT,
|
|
570
|
+
verified: false,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
if (options.cliActor) {
|
|
574
|
+
return {
|
|
575
|
+
actor: options.cliActor,
|
|
576
|
+
source: ActorSource.CLI_FLAG,
|
|
577
|
+
verified: false,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (options.configActor) {
|
|
581
|
+
return {
|
|
582
|
+
actor: options.configActor,
|
|
583
|
+
source: ActorSource.CONFIG,
|
|
584
|
+
verified: false,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
if (options.elementCreatedBy) {
|
|
588
|
+
return {
|
|
589
|
+
actor: options.elementCreatedBy,
|
|
590
|
+
source: ActorSource.ELEMENT,
|
|
591
|
+
verified: false,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
// No actor could be resolved
|
|
595
|
+
throw new ValidationError('No actor could be resolved. Provide an actor explicitly, via CLI flag (--actor), or in configuration.', ErrorCode.MISSING_REQUIRED_FIELD, { field: 'actor' });
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Validates an actor in soft identity mode
|
|
599
|
+
*
|
|
600
|
+
* In soft mode:
|
|
601
|
+
* - Accepts any non-empty string as actor
|
|
602
|
+
* - Optionally checks if entity exists (if lookupEntity provided and allowUnregisteredActors is false)
|
|
603
|
+
* - Always returns verified: false
|
|
604
|
+
*
|
|
605
|
+
* @param actor - The actor name/ID to validate
|
|
606
|
+
* @param options - Validation options
|
|
607
|
+
* @returns Validation result with context if valid
|
|
608
|
+
*/
|
|
609
|
+
export async function validateSoftActor(actor, options) {
|
|
610
|
+
const config = { ...DEFAULT_IDENTITY_CONFIG, ...options?.config };
|
|
611
|
+
// Basic validation - must be non-empty string
|
|
612
|
+
if (!actor || typeof actor !== 'string' || actor.trim().length === 0) {
|
|
613
|
+
return {
|
|
614
|
+
valid: false,
|
|
615
|
+
error: 'Actor must be a non-empty string',
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
// In soft mode with unregistered actors allowed, skip lookup
|
|
619
|
+
if (config.mode === IdentityMode.SOFT && config.allowUnregisteredActors) {
|
|
620
|
+
return {
|
|
621
|
+
valid: true,
|
|
622
|
+
context: {
|
|
623
|
+
actor,
|
|
624
|
+
source: ActorSource.EXPLICIT,
|
|
625
|
+
verified: false,
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
// If lookupEntity is provided and unregistered actors not allowed, verify entity exists
|
|
630
|
+
if (options?.lookupEntity && !config.allowUnregisteredActors) {
|
|
631
|
+
const entity = await options.lookupEntity(actor);
|
|
632
|
+
if (!entity) {
|
|
633
|
+
return {
|
|
634
|
+
valid: false,
|
|
635
|
+
error: `Actor '${actor}' not found and unregistered actors are not allowed`,
|
|
636
|
+
entityExists: false,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
valid: true,
|
|
641
|
+
context: {
|
|
642
|
+
actor,
|
|
643
|
+
source: ActorSource.EXPLICIT,
|
|
644
|
+
verified: false,
|
|
645
|
+
entityId: actor, // In soft mode, actor name is used as ID reference
|
|
646
|
+
},
|
|
647
|
+
entityExists: true,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
// Default: accept the actor
|
|
651
|
+
return {
|
|
652
|
+
valid: true,
|
|
653
|
+
context: {
|
|
654
|
+
actor,
|
|
655
|
+
source: ActorSource.EXPLICIT,
|
|
656
|
+
verified: false,
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Type guard for ActorContext
|
|
662
|
+
*/
|
|
663
|
+
export function isActorContext(value) {
|
|
664
|
+
if (typeof value !== 'object' || value === null) {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
const obj = value;
|
|
668
|
+
return (typeof obj.actor === 'string' &&
|
|
669
|
+
typeof obj.source === 'string' &&
|
|
670
|
+
Object.values(ActorSource).includes(obj.source) &&
|
|
671
|
+
typeof obj.verified === 'boolean');
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Creates an actor context for system operations
|
|
675
|
+
*/
|
|
676
|
+
export function createSystemActorContext() {
|
|
677
|
+
return {
|
|
678
|
+
actor: 'system',
|
|
679
|
+
source: ActorSource.SYSTEM,
|
|
680
|
+
verified: true, // System is always trusted
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Creates an actor context from an explicit actor string
|
|
685
|
+
*/
|
|
686
|
+
export function createActorContext(actor, source = ActorSource.EXPLICIT) {
|
|
687
|
+
return {
|
|
688
|
+
actor,
|
|
689
|
+
source,
|
|
690
|
+
verified: false,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Creates a verification middleware function
|
|
695
|
+
*
|
|
696
|
+
* The middleware:
|
|
697
|
+
* 1. Checks identity mode from config
|
|
698
|
+
* 2. In soft mode: allows all requests, extracts actor if provided
|
|
699
|
+
* 3. In cryptographic mode: requires valid signature
|
|
700
|
+
* 4. In hybrid mode: allows unsigned or validly signed requests
|
|
701
|
+
*
|
|
702
|
+
* @param options - Middleware options
|
|
703
|
+
* @returns A middleware function that verifies requests
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* ```typescript
|
|
707
|
+
* const middleware = createVerificationMiddleware({
|
|
708
|
+
* lookupEntity: (actor) => api.lookupEntityByName(actor),
|
|
709
|
+
* config: { mode: IdentityMode.CRYPTOGRAPHIC }
|
|
710
|
+
* });
|
|
711
|
+
*
|
|
712
|
+
* const result = await middleware(request);
|
|
713
|
+
* if (!result.allowed) {
|
|
714
|
+
* throw new Error(result.error);
|
|
715
|
+
* }
|
|
716
|
+
* // Use result.context.actor for the verified actor
|
|
717
|
+
* ```
|
|
718
|
+
*/
|
|
719
|
+
export function createVerificationMiddleware(options) {
|
|
720
|
+
const config = createIdentityConfig(options.config);
|
|
721
|
+
return async (request) => {
|
|
722
|
+
const { signedRequest, body } = request;
|
|
723
|
+
const mode = config.mode;
|
|
724
|
+
// Case 1: Soft mode - always allow, extract actor if provided
|
|
725
|
+
if (mode === IdentityMode.SOFT) {
|
|
726
|
+
return {
|
|
727
|
+
allowed: true,
|
|
728
|
+
context: {
|
|
729
|
+
actor: signedRequest?.actor,
|
|
730
|
+
verified: false,
|
|
731
|
+
mode,
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
// Case 2: No signed request provided
|
|
736
|
+
if (!signedRequest) {
|
|
737
|
+
if (mode === IdentityMode.CRYPTOGRAPHIC) {
|
|
738
|
+
return {
|
|
739
|
+
allowed: false,
|
|
740
|
+
context: {
|
|
741
|
+
verified: false,
|
|
742
|
+
mode,
|
|
743
|
+
},
|
|
744
|
+
error: 'Signature required in cryptographic mode',
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
// Hybrid mode: allow unsigned requests
|
|
748
|
+
return {
|
|
749
|
+
allowed: true,
|
|
750
|
+
context: {
|
|
751
|
+
verified: false,
|
|
752
|
+
mode,
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
// Case 3: Signed request provided - verify it
|
|
757
|
+
// Compute request hash
|
|
758
|
+
let requestHash;
|
|
759
|
+
try {
|
|
760
|
+
requestHash = await hashRequestBody(body ?? '');
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
return {
|
|
764
|
+
allowed: false,
|
|
765
|
+
context: {
|
|
766
|
+
verified: false,
|
|
767
|
+
mode,
|
|
768
|
+
},
|
|
769
|
+
error: 'Failed to compute request hash',
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
// Perform verification
|
|
773
|
+
const verificationResult = await verifySignature({
|
|
774
|
+
signedRequest,
|
|
775
|
+
requestHash,
|
|
776
|
+
lookupEntity: options.lookupEntity,
|
|
777
|
+
config,
|
|
778
|
+
now: options.now,
|
|
779
|
+
});
|
|
780
|
+
// Check if request should be allowed
|
|
781
|
+
const allowed = shouldAllowRequest(mode, verificationResult);
|
|
782
|
+
return {
|
|
783
|
+
allowed,
|
|
784
|
+
context: {
|
|
785
|
+
actor: verificationResult.status === VerificationStatus.VALID
|
|
786
|
+
? verificationResult.actor
|
|
787
|
+
: signedRequest.actor,
|
|
788
|
+
verified: verificationResult.status === VerificationStatus.VALID,
|
|
789
|
+
verificationResult,
|
|
790
|
+
mode,
|
|
791
|
+
},
|
|
792
|
+
error: allowed ? undefined : verificationResult.error ?? 'Verification failed',
|
|
793
|
+
};
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Creates middleware context for an unsigned request in soft mode
|
|
798
|
+
*/
|
|
799
|
+
export function createSoftModeContext(actor) {
|
|
800
|
+
return {
|
|
801
|
+
actor,
|
|
802
|
+
verified: false,
|
|
803
|
+
mode: IdentityMode.SOFT,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Creates middleware context for a verified request
|
|
808
|
+
*/
|
|
809
|
+
export function createVerifiedContext(actor, verificationResult) {
|
|
810
|
+
return {
|
|
811
|
+
actor,
|
|
812
|
+
verified: true,
|
|
813
|
+
verificationResult,
|
|
814
|
+
mode: IdentityMode.CRYPTOGRAPHIC,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
//# sourceMappingURL=identity.js.map
|