@metalabel/dfos-protocol 0.7.1 → 0.8.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.
@@ -1,8 +1,10 @@
1
1
  import {
2
- VC_TYPE_CONTENT_WRITE,
3
- decodeCredentialUnsafe,
4
- verifyCredential
5
- } from "./chunk-CZSEEZLL.js";
2
+ decodeDFOSCredentialUnsafe,
3
+ decodeMultikey,
4
+ matchesResource,
5
+ verifyDFOSCredential,
6
+ verifyDelegationChain
7
+ } from "./chunk-MEV6QVLC.js";
6
8
  import {
7
9
  createJws,
8
10
  dagCborCanonicalEncode,
@@ -79,7 +81,7 @@ var ContentUpdate = z.strictObject({
79
81
  baseDocumentCID: CIDString.nullable(),
80
82
  createdAt: Iso8601,
81
83
  note: z.string().max(MAX_NOTE).nullable(),
82
- /** VC-JWT authorizing this operation when signer is not the chain creator */
84
+ /** DFOS credential authorizing this operation when signer is not the chain creator */
83
85
  authorization: z.string().optional()
84
86
  });
85
87
  var ContentDelete = z.strictObject({
@@ -89,7 +91,7 @@ var ContentDelete = z.strictObject({
89
91
  previousOperationCID: CIDString,
90
92
  createdAt: Iso8601,
91
93
  note: z.string().max(MAX_NOTE).nullable(),
92
- /** VC-JWT authorizing this operation when signer is not the chain creator */
94
+ /** DFOS credential authorizing this operation when signer is not the chain creator */
93
95
  authorization: z.string().optional()
94
96
  });
95
97
  var ContentOperation = z.discriminatedUnion("type", [
@@ -101,7 +103,7 @@ var BeaconPayload = z.strictObject({
101
103
  version: z.literal(1),
102
104
  type: z.literal("beacon"),
103
105
  did: z.string().max(MAX_DID),
104
- merkleRoot: z.string().regex(/^[0-9a-f]{64}$/),
106
+ manifestContentId: z.string().max(MAX_CID),
105
107
  createdAt: Iso8601
106
108
  });
107
109
  var MAX_SCHEMA = 256;
@@ -121,45 +123,13 @@ var CountersignPayload = z.strictObject({
121
123
  targetCID: CIDString,
122
124
  createdAt: Iso8601
123
125
  });
124
-
125
- // src/chain/multikey.ts
126
- import { base58btc } from "multiformats/bases/base58";
127
- var ED25519_PUB_PREFIX = new Uint8Array([237, 1]);
128
- var ED25519_PRIV_PREFIX = new Uint8Array([128, 38]);
129
- var ED25519_PUB_MULTICODEC = 237;
130
- var ED25519_PRIV_MULTICODEC = 4864;
131
- var encodeEd25519Multikey = (publicKeyBytes) => {
132
- if (publicKeyBytes.length !== 32) {
133
- throw new Error(`expected 32-byte Ed25519 public key, got ${publicKeyBytes.length}`);
134
- }
135
- const prefixed = new Uint8Array(ED25519_PUB_PREFIX.length + publicKeyBytes.length);
136
- prefixed.set(ED25519_PUB_PREFIX);
137
- prefixed.set(publicKeyBytes, ED25519_PUB_PREFIX.length);
138
- return base58btc.encode(prefixed);
139
- };
140
- var decodeMultikey = (multibase) => {
141
- const bytes = base58btc.decode(multibase);
142
- if (bytes.length < 2) {
143
- throw new Error("multikey too short");
144
- }
145
- if (bytes[0] === ED25519_PUB_PREFIX[0] && bytes[1] === ED25519_PUB_PREFIX[1]) {
146
- const keyBytes = bytes.slice(2);
147
- if (keyBytes.length !== 32) {
148
- throw new Error(`expected 32-byte Ed25519 public key, got ${keyBytes.length}`);
149
- }
150
- return { keyBytes, codec: ED25519_PUB_MULTICODEC };
151
- }
152
- if (bytes[0] === ED25519_PRIV_PREFIX[0] && bytes[1] === ED25519_PRIV_PREFIX[1]) {
153
- const keyBytes = bytes.slice(2);
154
- if (keyBytes.length !== 32) {
155
- throw new Error(`expected 32-byte Ed25519 private key, got ${keyBytes.length}`);
156
- }
157
- return { keyBytes, codec: ED25519_PRIV_MULTICODEC };
158
- }
159
- throw new Error(
160
- `unsupported multikey codec: [0x${bytes[0]?.toString(16)}, 0x${bytes[1]?.toString(16)}]`
161
- );
162
- };
126
+ var RevocationPayload = z.strictObject({
127
+ version: z.literal(1),
128
+ type: z.literal("revocation"),
129
+ did: z.string().max(MAX_DID),
130
+ credentialCID: CIDString,
131
+ createdAt: Iso8601
132
+ });
163
133
 
164
134
  // src/chain/derivation.ts
165
135
  var deriveChainIdentifier = (cidBytes, prefix) => {
@@ -400,8 +370,39 @@ var signContentOperation = async (input) => {
400
370
  });
401
371
  return { jwsToken, operationCID };
402
372
  };
373
+ var verifyOperationAuthorization = async (input) => {
374
+ const decoded = decodeDFOSCredentialUnsafe(input.authorization);
375
+ if (!decoded) {
376
+ throw new Error("failed to decode authorization credential");
377
+ }
378
+ if (decoded.header.typ !== "did:dfos:credential") {
379
+ throw new Error(`invalid authorization typ: ${decoded.header.typ}`);
380
+ }
381
+ const opCreatedAtUnix = Math.floor(new Date(input.createdAt).getTime() / 1e3);
382
+ const credential = await verifyDFOSCredential(input.authorization, {
383
+ resolveIdentity: input.resolveIdentity,
384
+ now: opCreatedAtUnix
385
+ });
386
+ await verifyDelegationChain(credential, {
387
+ resolveIdentity: input.resolveIdentity,
388
+ rootDID: input.creatorDID,
389
+ now: opCreatedAtUnix
390
+ });
391
+ if (credential.aud !== "*" && credential.aud !== input.operationDID) {
392
+ throw new Error(
393
+ `credential audience ${credential.aud} does not match operation signer ${input.operationDID}`
394
+ );
395
+ }
396
+ const covers = await matchesResource(credential.att, `chain:${input.contentId}`, "write");
397
+ if (!covers) {
398
+ throw new Error(`credential does not cover write access to chain:${input.contentId}`);
399
+ }
400
+ };
403
401
  var verifyContentChain = async (input) => {
404
402
  if (input.log.length === 0) throw new Error("log must have at least one operation");
403
+ if (input.enforceAuthorization && !input.resolveIdentity) {
404
+ throw new Error("resolveIdentity is required when enforceAuthorization is true");
405
+ }
405
406
  const state = {
406
407
  contentId: null,
407
408
  genesisCID: null,
@@ -459,40 +460,18 @@ var verifyContentChain = async (input) => {
459
460
  const authorization = op.type !== "create" ? op.authorization : void 0;
460
461
  if (!authorization) {
461
462
  throw new Error(
462
- `log[${idx}]: signer ${op.did} is not the chain creator \u2014 authorization VC required`
463
+ `log[${idx}]: signer ${op.did} is not the chain creator \u2014 authorization credential required`
463
464
  );
464
465
  }
465
- const vcDecoded = decodeCredentialUnsafe(authorization);
466
- if (!vcDecoded) {
467
- throw new Error(`log[${idx}]: failed to decode authorization VC`);
468
- }
469
- const vcKid = vcDecoded.header.kid;
470
- if (!vcKid || !vcKid.includes("#")) {
471
- throw new Error(`log[${idx}]: authorization VC kid must be a DID URL`);
472
- }
473
- let creatorPublicKey;
474
- try {
475
- creatorPublicKey = await input.resolveKey(vcKid);
476
- } catch {
477
- throw new Error(`log[${idx}]: cannot resolve creator key for authorization verification`);
478
- }
479
- const opCreatedAtUnix = Math.floor(new Date(op.createdAt).getTime() / 1e3);
480
466
  try {
481
- const credential = verifyCredential({
482
- token: authorization,
483
- publicKey: creatorPublicKey,
484
- subject: op.did,
485
- expectedType: VC_TYPE_CONTENT_WRITE,
486
- currentTime: opCreatedAtUnix
467
+ await verifyOperationAuthorization({
468
+ authorization,
469
+ operationDID: op.did,
470
+ creatorDID: state.creatorDID,
471
+ contentId: state.contentId,
472
+ createdAt: op.createdAt,
473
+ resolveIdentity: input.resolveIdentity
487
474
  });
488
- if (credential.iss !== state.creatorDID) {
489
- throw new Error("VC issuer is not the chain creator");
490
- }
491
- if (credential.contentId && credential.contentId !== state.contentId) {
492
- throw new Error(
493
- `VC contentId ${credential.contentId} does not match chain ${state.contentId}`
494
- );
495
- }
496
475
  } catch (err) {
497
476
  const message = err instanceof Error ? err.message : "unknown error";
498
477
  throw new Error(`log[${idx}]: authorization verification failed: ${message}`);
@@ -541,6 +520,9 @@ var verifyContentExtensionFromTrustedState = async (input) => {
541
520
  if (currentState.isDeleted) {
542
521
  throw new Error("cannot extend a deleted chain");
543
522
  }
523
+ if (input.enforceAuthorization && !input.resolveIdentity) {
524
+ throw new Error("resolveIdentity is required when enforceAuthorization is true");
525
+ }
544
526
  const decoded = decodeJwsUnsafe(newOp);
545
527
  if (!decoded) throw new Error("failed to decode JWS");
546
528
  const result = ContentOperation.safeParse(decoded.payload);
@@ -577,37 +559,19 @@ var verifyContentExtensionFromTrustedState = async (input) => {
577
559
  if (op.did !== currentState.creatorDID && input.enforceAuthorization) {
578
560
  const authorization = op.authorization;
579
561
  if (!authorization) {
580
- throw new Error(`signer ${op.did} is not the chain creator \u2014 authorization VC required`);
581
- }
582
- const vcDecoded = decodeCredentialUnsafe(authorization);
583
- if (!vcDecoded) throw new Error("failed to decode authorization VC");
584
- const vcKid = vcDecoded.header.kid;
585
- if (!vcKid || !vcKid.includes("#")) {
586
- throw new Error("authorization VC kid must be a DID URL");
587
- }
588
- let creatorPublicKey;
589
- try {
590
- creatorPublicKey = await resolveKey(vcKid);
591
- } catch {
592
- throw new Error("cannot resolve creator key for authorization verification");
562
+ throw new Error(
563
+ `signer ${op.did} is not the chain creator \u2014 authorization credential required`
564
+ );
593
565
  }
594
- const opCreatedAtUnix = Math.floor(new Date(op.createdAt).getTime() / 1e3);
595
566
  try {
596
- const credential = verifyCredential({
597
- token: authorization,
598
- publicKey: creatorPublicKey,
599
- subject: op.did,
600
- expectedType: VC_TYPE_CONTENT_WRITE,
601
- currentTime: opCreatedAtUnix
567
+ await verifyOperationAuthorization({
568
+ authorization,
569
+ operationDID: op.did,
570
+ creatorDID: currentState.creatorDID,
571
+ contentId: currentState.contentId,
572
+ createdAt: op.createdAt,
573
+ resolveIdentity: input.resolveIdentity
602
574
  });
603
- if (credential.iss !== currentState.creatorDID) {
604
- throw new Error("VC issuer is not the chain creator");
605
- }
606
- if (credential.contentId && credential.contentId !== currentState.contentId) {
607
- throw new Error(
608
- `VC contentId ${credential.contentId} does not match chain ${currentState.contentId}`
609
- );
610
- }
611
575
  } catch (err) {
612
576
  const message = err instanceof Error ? err.message : "unknown error";
613
577
  throw new Error(`authorization verification failed: ${message}`);
@@ -675,7 +639,13 @@ var verifyBeacon = async (input) => {
675
639
  if (beaconTime > now + MAX_FUTURE_MS) {
676
640
  throw new Error("beacon createdAt is too far in the future");
677
641
  }
678
- return { payload, beaconCID };
642
+ return {
643
+ did: payload.did,
644
+ manifestContentId: payload.manifestContentId,
645
+ createdAt: payload.createdAt,
646
+ signerKeyId: kid,
647
+ beaconCID
648
+ };
679
649
  };
680
650
 
681
651
  // src/chain/countersign.ts
@@ -778,6 +748,64 @@ var verifyArtifact = async (input) => {
778
748
  return { payload, artifactCID };
779
749
  };
780
750
 
751
+ // src/chain/revocation.ts
752
+ var signRevocation = async (input) => {
753
+ const kid = `${input.issuerDID}#${input.keyId}`;
754
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\d{3}Z$/, "000Z");
755
+ const payload = {
756
+ version: 1,
757
+ type: "revocation",
758
+ did: input.issuerDID,
759
+ credentialCID: input.credentialCID,
760
+ createdAt: now
761
+ };
762
+ const encoded = await dagCborCanonicalEncode(payload);
763
+ const revocationCID = encoded.cid.toString();
764
+ const jwsToken = await createJws({
765
+ header: { alg: "EdDSA", typ: "did:dfos:revocation", kid, cid: revocationCID },
766
+ payload,
767
+ sign: input.signer
768
+ });
769
+ return { jwsToken, revocationCID };
770
+ };
771
+ var verifyRevocation = async (input) => {
772
+ const decoded = decodeJwsUnsafe(input.jwsToken);
773
+ if (!decoded) throw new Error("failed to decode revocation JWS");
774
+ const result = RevocationPayload.safeParse(decoded.payload);
775
+ if (!result.success) {
776
+ const messages = result.error.issues.map((e) => e.message).join(", ");
777
+ throw new Error(`invalid revocation payload: ${messages}`);
778
+ }
779
+ const payload = result.data;
780
+ if (decoded.header.typ !== "did:dfos:revocation") {
781
+ throw new Error(`invalid revocation typ: ${decoded.header.typ}`);
782
+ }
783
+ const kid = decoded.header.kid;
784
+ const hashIdx = kid.indexOf("#");
785
+ if (hashIdx < 0) throw new Error("revocation kid must be a DID URL");
786
+ const kidDid = kid.substring(0, hashIdx);
787
+ if (kidDid !== payload.did) {
788
+ throw new Error("revocation kid DID does not match payload did");
789
+ }
790
+ const publicKey = await input.resolveKey(kid);
791
+ try {
792
+ verifyJws({ token: input.jwsToken, publicKey });
793
+ } catch {
794
+ throw new Error("invalid revocation signature");
795
+ }
796
+ const encoded = await dagCborCanonicalEncode(payload);
797
+ const revocationCID = encoded.cid.toString();
798
+ if (!decoded.header.cid) throw new Error("missing cid in revocation header");
799
+ if (decoded.header.cid !== revocationCID) throw new Error("revocation cid mismatch");
800
+ return {
801
+ did: payload.did,
802
+ credentialCID: payload.credentialCID,
803
+ createdAt: payload.createdAt,
804
+ signerKeyId: kid,
805
+ revocationCID
806
+ };
807
+ };
808
+
781
809
  export {
782
810
  MultikeyPublicKey,
783
811
  IdentityOperation,
@@ -787,10 +815,7 @@ export {
787
815
  MAX_ARTIFACT_PAYLOAD_SIZE,
788
816
  ArtifactPayload,
789
817
  CountersignPayload,
790
- ED25519_PUB_MULTICODEC,
791
- ED25519_PRIV_MULTICODEC,
792
- encodeEd25519Multikey,
793
- decodeMultikey,
818
+ RevocationPayload,
794
819
  deriveChainIdentifier,
795
820
  deriveContentId,
796
821
  signIdentityOperation,
@@ -804,5 +829,7 @@ export {
804
829
  signCountersignature,
805
830
  verifyCountersignature,
806
831
  signArtifact,
807
- verifyArtifact
832
+ verifyArtifact,
833
+ signRevocation,
834
+ verifyRevocation
808
835
  };