@totemsdk/identity 0.1.0 → 0.1.1
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 +21 -0
- package/README.md +306 -0
- package/dist/canonical.js +6 -2
- package/dist/claims.js +15 -9
- package/dist/constants.js +4 -1
- package/dist/document.js +12 -8
- package/dist/guards.js +12 -5
- package/dist/index.js +32 -10
- package/dist/manifest-binding.js +11 -7
- package/dist/resolver.js +6 -3
- package/dist/revocation.js +6 -3
- package/dist/rotation.js +6 -3
- package/dist/signing.js +16 -12
- package/dist/types.js +2 -1
- package/dist/verify.js +13 -10
- package/package.json +29 -6
- package/src/__tests__/identity.test.ts +0 -618
- package/src/canonical.ts +0 -27
- package/src/claims.ts +0 -108
- package/src/constants.ts +0 -1
- package/src/document.ts +0 -35
- package/src/guards.ts +0 -75
- package/src/index.ts +0 -55
- package/src/manifest-binding.ts +0 -163
- package/src/resolver.ts +0 -171
- package/src/revocation.ts +0 -25
- package/src/rotation.ts +0 -23
- package/src/signing.ts +0 -38
- package/src/types.ts +0 -147
- package/src/verify.ts +0 -90
package/src/claims.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claim creation helpers.
|
|
3
|
-
*
|
|
4
|
-
* Claim IDs are deterministic: SHA3-256 of claim fields + canonical payload hash.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
8
|
-
import { canonicalJson, toHex } from './canonical.js';
|
|
9
|
-
import type { IdentityClaim, IdentityClaimType } from './types.js';
|
|
10
|
-
|
|
11
|
-
function computeClaimId(
|
|
12
|
-
type: IdentityClaimType,
|
|
13
|
-
issuer: string,
|
|
14
|
-
subject: string,
|
|
15
|
-
object: string,
|
|
16
|
-
issuedAt: number,
|
|
17
|
-
payload: Record<string, unknown>,
|
|
18
|
-
): string {
|
|
19
|
-
const canonical = canonicalJson({ type, issuer, subject, object, issuedAt, payload });
|
|
20
|
-
const hash = sha3_256(new TextEncoder().encode(canonical));
|
|
21
|
-
return toHex(hash);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function createIdentityClaim(opts: {
|
|
25
|
-
type: IdentityClaimType;
|
|
26
|
-
issuer: string;
|
|
27
|
-
subject: string;
|
|
28
|
-
object: string;
|
|
29
|
-
payload: Record<string, unknown>;
|
|
30
|
-
issuedAt?: number;
|
|
31
|
-
expiresAt?: number;
|
|
32
|
-
}): IdentityClaim {
|
|
33
|
-
const { type, issuer, subject, object, payload, expiresAt } = opts;
|
|
34
|
-
const issuedAt = opts.issuedAt ?? Date.now();
|
|
35
|
-
const id = computeClaimId(type, issuer, subject, object, issuedAt, payload);
|
|
36
|
-
return {
|
|
37
|
-
id,
|
|
38
|
-
type,
|
|
39
|
-
issuer,
|
|
40
|
-
subject,
|
|
41
|
-
object,
|
|
42
|
-
issuedAt,
|
|
43
|
-
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
44
|
-
payload,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function createDelegationClaim(opts: {
|
|
49
|
-
issuer: string;
|
|
50
|
-
subject: string;
|
|
51
|
-
delegatedAddress: string;
|
|
52
|
-
scopes: string[];
|
|
53
|
-
issuedAt?: number;
|
|
54
|
-
expiresAt?: number;
|
|
55
|
-
}): IdentityClaim {
|
|
56
|
-
const { issuer, subject, delegatedAddress, scopes, expiresAt } = opts;
|
|
57
|
-
return createIdentityClaim({
|
|
58
|
-
type: 'delegates_to',
|
|
59
|
-
issuer,
|
|
60
|
-
subject,
|
|
61
|
-
object: delegatedAddress,
|
|
62
|
-
payload: { scopes },
|
|
63
|
-
issuedAt: opts.issuedAt,
|
|
64
|
-
expiresAt,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function createPaymentRecipientClaim(opts: {
|
|
69
|
-
issuer: string;
|
|
70
|
-
subject: string;
|
|
71
|
-
address: string;
|
|
72
|
-
label?: string;
|
|
73
|
-
issuedAt?: number;
|
|
74
|
-
expiresAt?: number;
|
|
75
|
-
}): IdentityClaim {
|
|
76
|
-
const { issuer, subject, address, label, expiresAt } = opts;
|
|
77
|
-
const payload: Record<string, unknown> = {};
|
|
78
|
-
if (label !== undefined) payload.label = label;
|
|
79
|
-
return createIdentityClaim({
|
|
80
|
-
type: 'payment_recipient',
|
|
81
|
-
issuer,
|
|
82
|
-
subject,
|
|
83
|
-
object: address,
|
|
84
|
-
payload,
|
|
85
|
-
issuedAt: opts.issuedAt,
|
|
86
|
-
expiresAt,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function createServiceEndpointClaim(opts: {
|
|
91
|
-
issuer: string;
|
|
92
|
-
subject: string;
|
|
93
|
-
endpointType: string;
|
|
94
|
-
uri: string;
|
|
95
|
-
issuedAt?: number;
|
|
96
|
-
expiresAt?: number;
|
|
97
|
-
}): IdentityClaim {
|
|
98
|
-
const { issuer, subject, endpointType, uri, expiresAt } = opts;
|
|
99
|
-
return createIdentityClaim({
|
|
100
|
-
type: 'service_endpoint',
|
|
101
|
-
issuer,
|
|
102
|
-
subject,
|
|
103
|
-
object: uri,
|
|
104
|
-
payload: { endpointType },
|
|
105
|
-
issuedAt: opts.issuedAt,
|
|
106
|
-
expiresAt,
|
|
107
|
-
});
|
|
108
|
-
}
|
package/src/constants.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const IDENTITY_VERSION = 1 as const;
|
package/src/document.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Identity document creation and ID computation.
|
|
3
|
-
*
|
|
4
|
-
* computeIdentityId: deterministic SHA3-256 of "totem-identity" + kind + rootAddress.
|
|
5
|
-
* Version is NOT part of the ID hash — schema version upgrades must not change the identity ID.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
9
|
-
import { IDENTITY_VERSION } from './constants.js';
|
|
10
|
-
import { toHex } from './canonical.js';
|
|
11
|
-
import type { IdentityKind, TotemIdentityDocument } from './types.js';
|
|
12
|
-
|
|
13
|
-
export function computeIdentityId(kind: IdentityKind, rootAddress: string): string {
|
|
14
|
-
const input = `totem-identity\0${kind}\0${rootAddress}`;
|
|
15
|
-
const hash = sha3_256(new TextEncoder().encode(input));
|
|
16
|
-
return `totem:id:${kind}:${toHex(hash)}`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function createIdentityDocument(opts: {
|
|
20
|
-
kind: IdentityKind;
|
|
21
|
-
rootAddress: string;
|
|
22
|
-
controllerAddress: string;
|
|
23
|
-
metadata?: Record<string, unknown>;
|
|
24
|
-
}): TotemIdentityDocument {
|
|
25
|
-
const { kind, rootAddress, controllerAddress, metadata } = opts;
|
|
26
|
-
return {
|
|
27
|
-
id: computeIdentityId(kind, rootAddress),
|
|
28
|
-
kind,
|
|
29
|
-
version: IDENTITY_VERSION,
|
|
30
|
-
rootAddress,
|
|
31
|
-
controllerAddress,
|
|
32
|
-
createdAt: Date.now(),
|
|
33
|
-
...(metadata !== undefined ? { metadata } : {}),
|
|
34
|
-
};
|
|
35
|
-
}
|
package/src/guards.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type guards for identity types.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type {
|
|
6
|
-
TotemIdentityDocument,
|
|
7
|
-
IdentityClaim,
|
|
8
|
-
SignedIdentityClaim,
|
|
9
|
-
RotationClaim,
|
|
10
|
-
RevocationClaim,
|
|
11
|
-
} from './types.js';
|
|
12
|
-
|
|
13
|
-
export function isTotemIdentityDocument(value: unknown): value is TotemIdentityDocument {
|
|
14
|
-
if (!value || typeof value !== 'object') return false;
|
|
15
|
-
const v = value as Record<string, unknown>;
|
|
16
|
-
return (
|
|
17
|
-
typeof v.id === 'string' &&
|
|
18
|
-
typeof v.kind === 'string' &&
|
|
19
|
-
typeof v.version === 'number' &&
|
|
20
|
-
typeof v.rootAddress === 'string' &&
|
|
21
|
-
typeof v.controllerAddress === 'string' &&
|
|
22
|
-
typeof v.createdAt === 'number'
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function isIdentityClaim(value: unknown): value is IdentityClaim {
|
|
27
|
-
if (!value || typeof value !== 'object') return false;
|
|
28
|
-
const v = value as Record<string, unknown>;
|
|
29
|
-
return (
|
|
30
|
-
typeof v.id === 'string' &&
|
|
31
|
-
typeof v.type === 'string' &&
|
|
32
|
-
typeof v.issuer === 'string' &&
|
|
33
|
-
typeof v.subject === 'string' &&
|
|
34
|
-
typeof v.object === 'string' &&
|
|
35
|
-
typeof v.issuedAt === 'number' &&
|
|
36
|
-
typeof v.payload === 'object' &&
|
|
37
|
-
v.payload !== null
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function isSignedIdentityClaim(value: unknown): value is SignedIdentityClaim {
|
|
42
|
-
if (!value || typeof value !== 'object') return false;
|
|
43
|
-
const v = value as Record<string, unknown>;
|
|
44
|
-
if (!isIdentityClaim(v.claim)) return false;
|
|
45
|
-
if (!v.proof || typeof v.proof !== 'object') return false;
|
|
46
|
-
const p = v.proof as Record<string, unknown>;
|
|
47
|
-
return (
|
|
48
|
-
typeof p.address === 'string' &&
|
|
49
|
-
typeof p.publicKey === 'string' &&
|
|
50
|
-
typeof p.signature === 'string'
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function isRotationClaim(value: unknown): value is RotationClaim {
|
|
55
|
-
if (!value || typeof value !== 'object') return false;
|
|
56
|
-
const v = value as Record<string, unknown>;
|
|
57
|
-
return (
|
|
58
|
-
typeof v.claimId === 'string' &&
|
|
59
|
-
typeof v.issuer === 'string' &&
|
|
60
|
-
typeof v.subject === 'string' &&
|
|
61
|
-
typeof v.newAddress === 'string' &&
|
|
62
|
-
typeof v.issuedAt === 'number'
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function isRevocationClaim(value: unknown): value is RevocationClaim {
|
|
67
|
-
if (!value || typeof value !== 'object') return false;
|
|
68
|
-
const v = value as Record<string, unknown>;
|
|
69
|
-
return (
|
|
70
|
-
typeof v.claimId === 'string' &&
|
|
71
|
-
typeof v.issuer === 'string' &&
|
|
72
|
-
typeof v.subject === 'string' &&
|
|
73
|
-
typeof v.issuedAt === 'number'
|
|
74
|
-
);
|
|
75
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module @totemsdk/identity
|
|
3
|
-
*
|
|
4
|
-
* Canonical identity and claims layer for Totem Edge.
|
|
5
|
-
* Pure package — no network, no DHT, no blockchain submission.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export { IDENTITY_VERSION } from './constants.js';
|
|
9
|
-
|
|
10
|
-
export type {
|
|
11
|
-
IdentityKind,
|
|
12
|
-
IdentityStatus,
|
|
13
|
-
TotemIdentityDocument,
|
|
14
|
-
IdentityClaim,
|
|
15
|
-
IdentityClaimType,
|
|
16
|
-
SignedIdentityClaim,
|
|
17
|
-
IdentityVerifyResult,
|
|
18
|
-
IdentityProofVerifier,
|
|
19
|
-
IdentityGraph,
|
|
20
|
-
ResolvedIdentity,
|
|
21
|
-
IdentityResolutionResult,
|
|
22
|
-
DelegationClaim,
|
|
23
|
-
PaymentRecipientClaim,
|
|
24
|
-
ServiceEndpointClaim,
|
|
25
|
-
RotationClaim,
|
|
26
|
-
RevocationClaim,
|
|
27
|
-
ManifestIdentityBinding,
|
|
28
|
-
} from './types.js';
|
|
29
|
-
|
|
30
|
-
export { computeIdentityId, createIdentityDocument } from './document.js';
|
|
31
|
-
|
|
32
|
-
export {
|
|
33
|
-
createIdentityClaim,
|
|
34
|
-
createDelegationClaim,
|
|
35
|
-
createPaymentRecipientClaim,
|
|
36
|
-
createServiceEndpointClaim,
|
|
37
|
-
} from './claims.js';
|
|
38
|
-
|
|
39
|
-
export { signIdentityClaim } from './signing.js';
|
|
40
|
-
export { verifyIdentityClaim } from './verify.js';
|
|
41
|
-
|
|
42
|
-
export { rotateIdentity } from './rotation.js';
|
|
43
|
-
export { revokeIdentity } from './revocation.js';
|
|
44
|
-
|
|
45
|
-
export { resolveIdentityGraph } from './resolver.js';
|
|
46
|
-
|
|
47
|
-
export { bindManifestToIdentity, verifyManifestIdentity } from './manifest-binding.js';
|
|
48
|
-
|
|
49
|
-
export {
|
|
50
|
-
isTotemIdentityDocument,
|
|
51
|
-
isIdentityClaim,
|
|
52
|
-
isSignedIdentityClaim,
|
|
53
|
-
isRotationClaim,
|
|
54
|
-
isRevocationClaim,
|
|
55
|
-
} from './guards.js';
|
package/src/manifest-binding.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Manifest identity binding.
|
|
3
|
-
*
|
|
4
|
-
* bindManifestToIdentity: verify a signed manifest against an identity graph.
|
|
5
|
-
* verifyManifestIdentity: core verification logic.
|
|
6
|
-
*
|
|
7
|
-
* Security model for valid signer addresses:
|
|
8
|
-
* A manifest signer is authorized if the signing address is one of:
|
|
9
|
-
* 1. The identity's rootAddress
|
|
10
|
-
* 2. The identity's controllerAddress
|
|
11
|
-
* 3. An address in authorizedAddresses (delegates with "manifest:sign" or "*" scope)
|
|
12
|
-
* 4. An address in result.provenAddresses returned by the root-identity proof verifier
|
|
13
|
-
*
|
|
14
|
-
* Note: controlledAddresses (delegates with any scope) are NOT included in the
|
|
15
|
-
* manifest-signing valid set. Only explicitly scoped manifest signers may bind.
|
|
16
|
-
*
|
|
17
|
-
* Verification order:
|
|
18
|
-
* 1. verifyManifest (manifest signature must be valid)
|
|
19
|
-
* 2. resolveIdentityGraph (traverse graph)
|
|
20
|
-
* 3. Optional root-identity proof verifier hooks
|
|
21
|
-
* 4. Address membership check
|
|
22
|
-
* 5. Identity status check (revoked = invalid)
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { verifyManifest, computeManifestId } from '@totemsdk/manifest';
|
|
26
|
-
import type { SignedManifest } from '@totemsdk/manifest';
|
|
27
|
-
import { resolveIdentityGraph } from './resolver.js';
|
|
28
|
-
import type {
|
|
29
|
-
IdentityGraph,
|
|
30
|
-
ManifestIdentityBinding,
|
|
31
|
-
IdentityProofVerifier,
|
|
32
|
-
} from './types.js';
|
|
33
|
-
|
|
34
|
-
export async function verifyManifestIdentity(
|
|
35
|
-
signedManifest: SignedManifest<any>,
|
|
36
|
-
identityGraph: IdentityGraph,
|
|
37
|
-
options?: {
|
|
38
|
-
proofVerifiers?: Record<string, IdentityProofVerifier>;
|
|
39
|
-
},
|
|
40
|
-
): Promise<ManifestIdentityBinding> {
|
|
41
|
-
const proofVerifiers = options?.proofVerifiers ?? {};
|
|
42
|
-
const manifestId = computeManifestId(signedManifest.manifest);
|
|
43
|
-
|
|
44
|
-
// Step 1: verify manifest signature first — fail fast on invalid manifest
|
|
45
|
-
const manifestVerifyResult = verifyManifest(signedManifest);
|
|
46
|
-
if (!manifestVerifyResult.valid) {
|
|
47
|
-
return {
|
|
48
|
-
valid: false,
|
|
49
|
-
reason: `manifest signature invalid: ${manifestVerifyResult.reason ?? 'unknown'}`,
|
|
50
|
-
manifestId,
|
|
51
|
-
identityId: identityGraph.document.id,
|
|
52
|
-
signerAddress: signedManifest.authorAddress,
|
|
53
|
-
resolvedStatus: 'active',
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Step 2: resolve identity graph (includes claim signature verification)
|
|
58
|
-
const resolution = resolveIdentityGraph(identityGraph);
|
|
59
|
-
if (!resolution.resolved) {
|
|
60
|
-
return {
|
|
61
|
-
valid: false,
|
|
62
|
-
reason: 'identity graph could not be resolved',
|
|
63
|
-
manifestId,
|
|
64
|
-
identityId: identityGraph.document.id,
|
|
65
|
-
signerAddress: signedManifest.authorAddress,
|
|
66
|
-
resolvedStatus: 'active',
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const resolved = resolution.resolved;
|
|
71
|
-
|
|
72
|
-
// Step 3: collect additional proven addresses from proof verifiers
|
|
73
|
-
const provenAddresses: string[] = [];
|
|
74
|
-
|
|
75
|
-
// Check manifest-level rootIdentityProof
|
|
76
|
-
if (signedManifest.rootIdentityProof && proofVerifiers['root-identity']) {
|
|
77
|
-
try {
|
|
78
|
-
const verifyResult = await proofVerifiers['root-identity'].verify(
|
|
79
|
-
signedManifest.rootIdentityProof,
|
|
80
|
-
);
|
|
81
|
-
if (verifyResult.valid && verifyResult.provenAddresses) {
|
|
82
|
-
provenAddresses.push(...verifyResult.provenAddresses);
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
// silently ignore verifier errors
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Check claim-level rootIdentityProof fields
|
|
90
|
-
if (proofVerifiers['root-identity']) {
|
|
91
|
-
for (const sc of identityGraph.claims) {
|
|
92
|
-
if (sc.rootIdentityProof) {
|
|
93
|
-
try {
|
|
94
|
-
const verifyResult = await proofVerifiers['root-identity'].verify(
|
|
95
|
-
sc.rootIdentityProof,
|
|
96
|
-
);
|
|
97
|
-
if (verifyResult.valid && verifyResult.provenAddresses) {
|
|
98
|
-
provenAddresses.push(...verifyResult.provenAddresses);
|
|
99
|
-
}
|
|
100
|
-
} catch {
|
|
101
|
-
// silently ignore verifier errors
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Step 4: check address validity
|
|
108
|
-
// Valid signers (per spec):
|
|
109
|
-
// 1. rootAddress
|
|
110
|
-
// 2. controllerAddress
|
|
111
|
-
// 3. controlledAddresses — all delegated addresses (any scope)
|
|
112
|
-
// 4. authorizedAddresses — subset with "manifest:sign" or "*" scope (already subset of above)
|
|
113
|
-
// 5. provenAddresses — from root-identity proof verifiers
|
|
114
|
-
const signerAddress = signedManifest.authorAddress;
|
|
115
|
-
const validAddresses = new Set<string>([
|
|
116
|
-
resolved.rootAddress,
|
|
117
|
-
resolved.controllerAddress,
|
|
118
|
-
...resolved.controlledAddresses,
|
|
119
|
-
...resolved.authorizedAddresses,
|
|
120
|
-
...provenAddresses,
|
|
121
|
-
]);
|
|
122
|
-
|
|
123
|
-
if (!validAddresses.has(signerAddress)) {
|
|
124
|
-
return {
|
|
125
|
-
valid: false,
|
|
126
|
-
reason: `signer address '${signerAddress}' is not authorized to sign manifests for identity '${identityGraph.document.id}'`,
|
|
127
|
-
manifestId,
|
|
128
|
-
identityId: identityGraph.document.id,
|
|
129
|
-
signerAddress,
|
|
130
|
-
resolvedStatus: resolved.status,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Step 5: check identity status — revoked identities cannot bind
|
|
135
|
-
if (resolved.status === 'revoked') {
|
|
136
|
-
return {
|
|
137
|
-
valid: false,
|
|
138
|
-
reason: 'identity has been revoked',
|
|
139
|
-
manifestId,
|
|
140
|
-
identityId: identityGraph.document.id,
|
|
141
|
-
signerAddress,
|
|
142
|
-
resolvedStatus: resolved.status,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
valid: true,
|
|
148
|
-
manifestId,
|
|
149
|
-
identityId: identityGraph.document.id,
|
|
150
|
-
signerAddress,
|
|
151
|
-
resolvedStatus: resolved.status,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export async function bindManifestToIdentity(
|
|
156
|
-
signedManifest: SignedManifest<any>,
|
|
157
|
-
identityGraph: IdentityGraph,
|
|
158
|
-
options?: {
|
|
159
|
-
proofVerifiers?: Record<string, IdentityProofVerifier>;
|
|
160
|
-
},
|
|
161
|
-
): Promise<ManifestIdentityBinding> {
|
|
162
|
-
return verifyManifestIdentity(signedManifest, identityGraph, options);
|
|
163
|
-
}
|
package/src/resolver.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local-only identity graph resolver.
|
|
3
|
-
*
|
|
4
|
-
* Resolves an IdentityGraph into a ResolvedIdentity by:
|
|
5
|
-
* - Verifying each claim's signature before accepting it
|
|
6
|
-
* - Enforcing claim authority (only root, controller, or delegated addresses may issue)
|
|
7
|
-
* - Detecting rotation/revocation
|
|
8
|
-
* - Collecting payment recipients, service endpoints, delegations
|
|
9
|
-
*
|
|
10
|
-
* Security: every SignedIdentityClaim is passed through verifyIdentityClaim before
|
|
11
|
-
* its issuer or content is trusted. Unsigned or tampered claims are silently dropped.
|
|
12
|
-
*
|
|
13
|
-
* Claim authority rules (after signature verification):
|
|
14
|
-
* A claim is only accepted if its issuer is:
|
|
15
|
-
* 1. The subject's rootAddress
|
|
16
|
-
* 2. The subject's controllerAddress
|
|
17
|
-
* 3. An address holding an active + signature-verified delegates_to claim from the subject
|
|
18
|
-
* Claims from unauthorized issuers are silently dropped.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { verifyIdentityClaim } from './verify.js';
|
|
22
|
-
import type {
|
|
23
|
-
IdentityGraph,
|
|
24
|
-
IdentityResolutionResult,
|
|
25
|
-
ResolvedIdentity,
|
|
26
|
-
IdentityStatus,
|
|
27
|
-
DelegationClaim,
|
|
28
|
-
PaymentRecipientClaim,
|
|
29
|
-
ServiceEndpointClaim,
|
|
30
|
-
SignedIdentityClaim,
|
|
31
|
-
} from './types.js';
|
|
32
|
-
|
|
33
|
-
function isExpired(claim: { expiresAt?: number }): boolean {
|
|
34
|
-
if (claim.expiresAt === undefined) return false;
|
|
35
|
-
return Date.now() > claim.expiresAt;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function resolveIdentityGraph(graph: IdentityGraph): IdentityResolutionResult {
|
|
39
|
-
const { document, claims } = graph;
|
|
40
|
-
const { rootAddress, controllerAddress } = document;
|
|
41
|
-
|
|
42
|
-
// Step 1: signature-verify all claims upfront and collect the valid ones.
|
|
43
|
-
// A claim is ONLY trusted when:
|
|
44
|
-
// (a) the WOTS signature is valid over the canonical claim bytes, AND
|
|
45
|
-
// (b) the cryptographic signer address (proof.address, bound to proof.publicKey via key
|
|
46
|
-
// derivation) exactly matches claim.issuer.
|
|
47
|
-
// This prevents issuer-field spoofing: an attacker cannot set claim.issuer to a privileged
|
|
48
|
-
// address (root, controller, delegate) if they signed with their own key.
|
|
49
|
-
const verifiedClaims = claims.filter((sc) => {
|
|
50
|
-
const result = verifyIdentityClaim(sc);
|
|
51
|
-
return result.valid && result.signerAddress === sc.claim.issuer;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Step 2: collect delegation claims issued by root or controller only (first-level authority).
|
|
55
|
-
const rootDelegations = verifiedClaims.filter(
|
|
56
|
-
(sc) =>
|
|
57
|
-
sc.claim.type === 'delegates_to' &&
|
|
58
|
-
sc.claim.subject === document.id &&
|
|
59
|
-
(sc.claim.issuer === rootAddress || sc.claim.issuer === controllerAddress) &&
|
|
60
|
-
!isExpired(sc.claim),
|
|
61
|
-
);
|
|
62
|
-
const firstLevelDelegates = new Set<string>(rootDelegations.map((sc) => sc.claim.object));
|
|
63
|
-
|
|
64
|
-
// Full authorized issuer set
|
|
65
|
-
const allAuthorized = new Set<string>([rootAddress, controllerAddress, ...firstLevelDelegates]);
|
|
66
|
-
|
|
67
|
-
// Helper: claim is authorized if its issuer is in the authorized set and targets this identity
|
|
68
|
-
function isAuthorized(sc: SignedIdentityClaim): boolean {
|
|
69
|
-
return allAuthorized.has(sc.claim.issuer) && sc.claim.subject === document.id;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Detect revocation
|
|
73
|
-
const revocationClaims = verifiedClaims.filter(
|
|
74
|
-
(sc) => sc.claim.type === 'revokes' && isAuthorized(sc),
|
|
75
|
-
);
|
|
76
|
-
const isRevoked = revocationClaims.length > 0;
|
|
77
|
-
const revokedAt = isRevoked
|
|
78
|
-
? Math.min(...revocationClaims.map((sc) => sc.claim.issuedAt))
|
|
79
|
-
: undefined;
|
|
80
|
-
|
|
81
|
-
// Detect rotation
|
|
82
|
-
const rotationClaims = verifiedClaims.filter(
|
|
83
|
-
(sc) => sc.claim.type === 'rotates_to' && isAuthorized(sc) && !isExpired(sc.claim),
|
|
84
|
-
);
|
|
85
|
-
const rotationTarget = rotationClaims.length > 0 ? rotationClaims[0].claim.object : undefined;
|
|
86
|
-
|
|
87
|
-
let status: IdentityStatus = 'active';
|
|
88
|
-
if (isRevoked) status = 'revoked';
|
|
89
|
-
else if (rotationTarget !== undefined) status = 'rotated';
|
|
90
|
-
|
|
91
|
-
// Collect all delegation claims from authorized issuers
|
|
92
|
-
const allDelegationSignedClaims = verifiedClaims.filter(
|
|
93
|
-
(sc) =>
|
|
94
|
-
sc.claim.type === 'delegates_to' &&
|
|
95
|
-
sc.claim.subject === document.id &&
|
|
96
|
-
allAuthorized.has(sc.claim.issuer) &&
|
|
97
|
-
!isExpired(sc.claim),
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
const delegates: DelegationClaim[] = allDelegationSignedClaims.map((sc) => ({
|
|
101
|
-
claimId: sc.claim.id,
|
|
102
|
-
issuer: sc.claim.issuer,
|
|
103
|
-
subject: sc.claim.subject,
|
|
104
|
-
delegatedAddress: sc.claim.object,
|
|
105
|
-
scopes: Array.isArray(sc.claim.payload.scopes) ? (sc.claim.payload.scopes as string[]) : [],
|
|
106
|
-
issuedAt: sc.claim.issuedAt,
|
|
107
|
-
expiresAt: sc.claim.expiresAt,
|
|
108
|
-
}));
|
|
109
|
-
|
|
110
|
-
// controlledAddresses: all delegated addresses (any scope) — for informational use
|
|
111
|
-
const controlledAddresses: string[] = [...new Set(delegates.map((d) => d.delegatedAddress))];
|
|
112
|
-
|
|
113
|
-
// authorizedAddresses: only delegates with manifest:sign or * scope
|
|
114
|
-
const authorizedAddresses: string[] = [
|
|
115
|
-
...new Set(
|
|
116
|
-
delegates
|
|
117
|
-
.filter((d) => d.scopes.includes('*') || d.scopes.includes('manifest:sign'))
|
|
118
|
-
.map((d) => d.delegatedAddress),
|
|
119
|
-
),
|
|
120
|
-
];
|
|
121
|
-
|
|
122
|
-
// Payment recipients
|
|
123
|
-
const paymentRecipients: PaymentRecipientClaim[] = verifiedClaims
|
|
124
|
-
.filter(
|
|
125
|
-
(sc) =>
|
|
126
|
-
sc.claim.type === 'payment_recipient' &&
|
|
127
|
-
isAuthorized(sc) &&
|
|
128
|
-
!isExpired(sc.claim),
|
|
129
|
-
)
|
|
130
|
-
.map((sc) => ({
|
|
131
|
-
claimId: sc.claim.id,
|
|
132
|
-
issuer: sc.claim.issuer,
|
|
133
|
-
address: sc.claim.object,
|
|
134
|
-
label: typeof sc.claim.payload.label === 'string' ? sc.claim.payload.label : undefined,
|
|
135
|
-
issuedAt: sc.claim.issuedAt,
|
|
136
|
-
expiresAt: sc.claim.expiresAt,
|
|
137
|
-
}));
|
|
138
|
-
|
|
139
|
-
// Service endpoints
|
|
140
|
-
const serviceEndpoints: ServiceEndpointClaim[] = verifiedClaims
|
|
141
|
-
.filter(
|
|
142
|
-
(sc) =>
|
|
143
|
-
sc.claim.type === 'service_endpoint' &&
|
|
144
|
-
isAuthorized(sc) &&
|
|
145
|
-
!isExpired(sc.claim),
|
|
146
|
-
)
|
|
147
|
-
.map((sc) => ({
|
|
148
|
-
claimId: sc.claim.id,
|
|
149
|
-
issuer: sc.claim.issuer,
|
|
150
|
-
endpointType: typeof sc.claim.payload.endpointType === 'string' ? sc.claim.payload.endpointType : 'unknown',
|
|
151
|
-
uri: sc.claim.object,
|
|
152
|
-
issuedAt: sc.claim.issuedAt,
|
|
153
|
-
expiresAt: sc.claim.expiresAt,
|
|
154
|
-
}));
|
|
155
|
-
|
|
156
|
-
const resolved: ResolvedIdentity = {
|
|
157
|
-
document,
|
|
158
|
-
status,
|
|
159
|
-
rootAddress,
|
|
160
|
-
controllerAddress,
|
|
161
|
-
controlledAddresses,
|
|
162
|
-
authorizedAddresses,
|
|
163
|
-
delegates,
|
|
164
|
-
paymentRecipients,
|
|
165
|
-
serviceEndpoints,
|
|
166
|
-
rotationTarget,
|
|
167
|
-
revokedAt,
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
return { resolved, errors: [] };
|
|
171
|
-
}
|
package/src/revocation.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Identity revocation — produces a RevocationClaim.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { createIdentityClaim } from './claims.js';
|
|
6
|
-
import type { IdentityClaim } from './types.js';
|
|
7
|
-
|
|
8
|
-
export function revokeIdentity(opts: {
|
|
9
|
-
issuer: string;
|
|
10
|
-
subject: string;
|
|
11
|
-
reason?: string;
|
|
12
|
-
issuedAt?: number;
|
|
13
|
-
}): IdentityClaim {
|
|
14
|
-
const { issuer, subject, reason } = opts;
|
|
15
|
-
const payload: Record<string, unknown> = {};
|
|
16
|
-
if (reason !== undefined) payload.reason = reason;
|
|
17
|
-
return createIdentityClaim({
|
|
18
|
-
type: 'revokes',
|
|
19
|
-
issuer,
|
|
20
|
-
subject,
|
|
21
|
-
object: subject,
|
|
22
|
-
payload,
|
|
23
|
-
issuedAt: opts.issuedAt,
|
|
24
|
-
});
|
|
25
|
-
}
|
package/src/rotation.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Identity rotation — produces a RotationClaim.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { createIdentityClaim } from './claims.js';
|
|
6
|
-
import type { IdentityClaim } from './types.js';
|
|
7
|
-
|
|
8
|
-
export function rotateIdentity(opts: {
|
|
9
|
-
issuer: string;
|
|
10
|
-
subject: string;
|
|
11
|
-
newAddress: string;
|
|
12
|
-
issuedAt?: number;
|
|
13
|
-
}): IdentityClaim {
|
|
14
|
-
const { issuer, subject, newAddress } = opts;
|
|
15
|
-
return createIdentityClaim({
|
|
16
|
-
type: 'rotates_to',
|
|
17
|
-
issuer,
|
|
18
|
-
subject,
|
|
19
|
-
object: newAddress,
|
|
20
|
-
payload: {},
|
|
21
|
-
issuedAt: opts.issuedAt,
|
|
22
|
-
});
|
|
23
|
-
}
|
package/src/signing.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claim signing using WOTS primitives from @totemsdk/core.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
6
|
-
import {
|
|
7
|
-
wotsSign,
|
|
8
|
-
wotsKeypairFromSeed,
|
|
9
|
-
wotsAddressFromKeypair,
|
|
10
|
-
bytesToHex,
|
|
11
|
-
} from '@totemsdk/core';
|
|
12
|
-
import { canonicalJson } from './canonical.js';
|
|
13
|
-
import type { IdentityClaim, SignedIdentityClaim } from './types.js';
|
|
14
|
-
|
|
15
|
-
export function claimDigest(claim: IdentityClaim): Uint8Array {
|
|
16
|
-
const canonical = canonicalJson(claim);
|
|
17
|
-
return sha3_256(new TextEncoder().encode(canonical));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function signIdentityClaim(
|
|
21
|
-
claim: IdentityClaim,
|
|
22
|
-
seed: Uint8Array,
|
|
23
|
-
keyIndex: number,
|
|
24
|
-
): Promise<SignedIdentityClaim> {
|
|
25
|
-
const digest = claimDigest(claim);
|
|
26
|
-
const sigBytes = wotsSign(seed, keyIndex, digest);
|
|
27
|
-
const kp = wotsKeypairFromSeed(seed, keyIndex);
|
|
28
|
-
const address = wotsAddressFromKeypair(kp);
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
claim,
|
|
32
|
-
proof: {
|
|
33
|
-
address,
|
|
34
|
-
publicKey: bytesToHex(kp.pk),
|
|
35
|
-
signature: bytesToHex(sigBytes),
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
}
|