@tinycloud/cli 1.5.0-beta.23 → 1.5.0-beta.25

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/dist/index.js CHANGED
@@ -13800,6 +13800,7 @@ init_profiles();
13800
13800
 
13801
13801
  // src/commands/share.ts
13802
13802
  import { Option } from "commander";
13803
+ import { readFile as readFile8 } from "fs/promises";
13803
13804
  import { CLI_NAME as CLI_NAME12 } from "@tinycloud/operations/cli-name";
13804
13805
 
13805
13806
  // ../share-sdk/dist/artifact.js
@@ -18986,6 +18987,23 @@ var unifiedEncryptionNetworkSchema = external_exports.string().refine((value) =>
18986
18987
  const network = separator < 0 ? "" : rest.slice(separator + 1);
18987
18988
  return owner.startsWith("did:") && owner.length > 4 && network.length > 0 && !/[:/%?#\s]/.test(network);
18988
18989
  }, { message: "expected canonical TinyCloud encryption network" });
18990
+ var unifiedEncryptionResourceSchema = external_exports.string().refine((value) => {
18991
+ const prefix = "urn:tinycloud:encryption:";
18992
+ if (!value.startsWith(prefix)) return false;
18993
+ const match = /^(did:.+):([^/:?#\s]+)(?:\/(.+))?$/.exec(value.slice(prefix.length));
18994
+ if (match === null) return false;
18995
+ const path = match[3];
18996
+ return path === void 0 || path.split("/").every(
18997
+ (segment) => segment.length > 0 && segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment)
18998
+ );
18999
+ }, { message: "expected a canonical TinyCloud encryption resource" });
19000
+ var unifiedEncryptionKeyPathSchema = external_exports.string().regex(/^share\/[^/]+$/).refine(
19001
+ (value) => {
19002
+ const segment = value.slice("share/".length);
19003
+ return segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment);
19004
+ },
19005
+ { message: "expected an exact share key path" }
19006
+ );
18989
19007
  var unifiedKvCapabilitySchema = external_exports.object({
18990
19008
  kind: external_exports.literal("kv"),
18991
19009
  resource: unifiedResourceSchema,
@@ -18999,7 +19017,7 @@ var unifiedKvCapabilitySchema = external_exports.object({
18999
19017
  }).strict();
19000
19018
  var unifiedEncryptionCapabilitySchema = external_exports.object({
19001
19019
  kind: external_exports.literal("encryption"),
19002
- resource: unifiedEncryptionNetworkSchema,
19020
+ resource: unifiedEncryptionResourceSchema,
19003
19021
  action: external_exports.literal("tinycloud.encryption/decrypt")
19004
19022
  }).strict();
19005
19023
  var unifiedCapabilitySchema = external_exports.union([unifiedKvCapabilitySchema, unifiedEncryptionCapabilitySchema]);
@@ -19008,6 +19026,7 @@ var unifiedContentSourceSchema = external_exports.object({
19008
19026
  kvResource: unifiedResourceSchema,
19009
19027
  selector: external_exports.union([external_exports.literal("exact"), external_exports.literal("prefix")]),
19010
19028
  encryptionNetwork: unifiedEncryptionNetworkSchema,
19029
+ encryptionKeyPath: unifiedEncryptionKeyPathSchema.optional(),
19011
19030
  encryptedSymmetricKeyDigestHex: external_exports.string().regex(/^[0-9a-f]{64}$/),
19012
19031
  keyVersion: external_exports.number().int().positive(),
19013
19032
  mode: external_exports.union([external_exports.literal("mutable"), external_exports.literal("immutable")]),
@@ -19125,16 +19144,21 @@ function validateV3Invariants(value, ctx) {
19125
19144
  if (value.attestedEnforcerBinding.nodeAudience !== value.target.nodeAudience || value.attestedEnforcerBinding.signature.signerDid !== value.attestedEnforcerBinding.nodeAudience || Date.parse(value.attestedEnforcerBinding.expiresAt) < Date.parse(value.expiry)) {
19126
19145
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["attestedEnforcerBinding"], message: "enforcer binding does not cover the target and share lifetime" });
19127
19146
  }
19128
- if (value.policy.contentSource.shareId !== value.contentSource.shareId || value.policy.contentSource.kvResource !== value.contentSource.kvResource || value.policy.contentSource.selector !== value.contentSource.selector || value.policy.contentSource.encryptionNetwork !== value.encryptionNetwork || value.policy.contentSource.encryptedSymmetricKeyDigestHex !== value.contentSource.encryptedSymmetricKeyDigestHex || value.policy.contentSource.keyVersion !== value.contentSource.keyVersion || value.policy.contentSource.mode !== value.contentSource.mode || value.policy.contentSource.initialCiphertextDigestHex !== value.contentSource.initialCiphertextDigestHex) {
19147
+ const encryptionKeyPath = value.contentSource.encryptionKeyPath;
19148
+ if (encryptionKeyPath !== void 0 && encryptionKeyPath !== `share/${value.contentSource.shareId}`) {
19149
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["contentSource", "encryptionKeyPath"], message: "share key path must name this share" });
19150
+ }
19151
+ if (value.policy.contentSource.shareId !== value.contentSource.shareId || value.policy.contentSource.kvResource !== value.contentSource.kvResource || value.policy.contentSource.selector !== value.contentSource.selector || value.policy.contentSource.encryptionNetwork !== value.encryptionNetwork || value.policy.contentSource.encryptionKeyPath !== encryptionKeyPath || value.policy.contentSource.encryptedSymmetricKeyDigestHex !== value.contentSource.encryptedSymmetricKeyDigestHex || value.policy.contentSource.keyVersion !== value.contentSource.keyVersion || value.policy.contentSource.mode !== value.contentSource.mode || value.policy.contentSource.initialCiphertextDigestHex !== value.contentSource.initialCiphertextDigestHex) {
19129
19152
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["contentSource"], message: "content source is not bound to policy" });
19130
19153
  }
19131
19154
  if (value.policy.signature.suite !== "Ed25519" || value.policy.signature.signerDid !== value.policy.ownerDid) {
19132
19155
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "signature"], message: "policy must be signed by its owner" });
19133
19156
  }
19134
19157
  const kvCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "kv");
19135
- const decryptCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "encryption" && capability.resource === value.encryptionNetwork && capability.action === "tinycloud.encryption/decrypt");
19158
+ const expectedEncryptionResource = encryptionKeyPath === void 0 ? value.encryptionNetwork : `${value.encryptionNetwork}/${encryptionKeyPath}`;
19159
+ const decryptCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "encryption" && capability.resource === expectedEncryptionResource && capability.action === "tinycloud.encryption/decrypt");
19136
19160
  if (value.policy.capabilityCeiling.length !== 2 || kvCapabilities.length !== 1 || decryptCapabilities.length !== 1 || kvCapabilities[0]?.resource !== value.contentSource.kvResource || kvCapabilities[0]?.selector !== value.contentSource.selector) {
19137
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain exact decrypt network" });
19161
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain the exact decrypt resource" });
19138
19162
  }
19139
19163
  }
19140
19164
  var unsignedShareEnvelopeV3Schema = unsignedShareEnvelopeV3BaseSchema.superRefine(validateV3Invariants);
@@ -24637,6 +24661,23 @@ var unifiedEncryptionNetworkSchema2 = external_exports3.string().refine((value)
24637
24661
  const network = separator < 0 ? "" : rest.slice(separator + 1);
24638
24662
  return owner.startsWith("did:") && owner.length > 4 && network.length > 0 && !/[:/%?#\s]/.test(network);
24639
24663
  }, { message: "expected canonical TinyCloud encryption network" });
24664
+ var unifiedEncryptionResourceSchema2 = external_exports3.string().refine((value) => {
24665
+ const prefix = "urn:tinycloud:encryption:";
24666
+ if (!value.startsWith(prefix)) return false;
24667
+ const match = /^(did:.+):([^/:?#\s]+)(?:\/(.+))?$/.exec(value.slice(prefix.length));
24668
+ if (match === null) return false;
24669
+ const path = match[3];
24670
+ return path === void 0 || path.split("/").every(
24671
+ (segment) => segment.length > 0 && segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment)
24672
+ );
24673
+ }, { message: "expected a canonical TinyCloud encryption resource" });
24674
+ var unifiedEncryptionKeyPathSchema2 = external_exports3.string().regex(/^share\/[^/]+$/).refine(
24675
+ (value) => {
24676
+ const segment = value.slice("share/".length);
24677
+ return segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment);
24678
+ },
24679
+ { message: "expected an exact share key path" }
24680
+ );
24640
24681
  var unifiedKvCapabilitySchema2 = external_exports3.object({
24641
24682
  kind: external_exports3.literal("kv"),
24642
24683
  resource: unifiedResourceSchema2,
@@ -24650,7 +24691,7 @@ var unifiedKvCapabilitySchema2 = external_exports3.object({
24650
24691
  }).strict();
24651
24692
  var unifiedEncryptionCapabilitySchema2 = external_exports3.object({
24652
24693
  kind: external_exports3.literal("encryption"),
24653
- resource: unifiedEncryptionNetworkSchema2,
24694
+ resource: unifiedEncryptionResourceSchema2,
24654
24695
  action: external_exports3.literal("tinycloud.encryption/decrypt")
24655
24696
  }).strict();
24656
24697
  var unifiedCapabilitySchema2 = external_exports3.union([unifiedKvCapabilitySchema2, unifiedEncryptionCapabilitySchema2]);
@@ -24659,6 +24700,7 @@ var unifiedContentSourceSchema2 = external_exports3.object({
24659
24700
  kvResource: unifiedResourceSchema2,
24660
24701
  selector: external_exports3.union([external_exports3.literal("exact"), external_exports3.literal("prefix")]),
24661
24702
  encryptionNetwork: unifiedEncryptionNetworkSchema2,
24703
+ encryptionKeyPath: unifiedEncryptionKeyPathSchema2.optional(),
24662
24704
  encryptedSymmetricKeyDigestHex: external_exports3.string().regex(/^[0-9a-f]{64}$/),
24663
24705
  keyVersion: external_exports3.number().int().positive(),
24664
24706
  mode: external_exports3.union([external_exports3.literal("mutable"), external_exports3.literal("immutable")]),
@@ -24776,16 +24818,21 @@ function validateV3Invariants2(value, ctx) {
24776
24818
  if (value.attestedEnforcerBinding.nodeAudience !== value.target.nodeAudience || value.attestedEnforcerBinding.signature.signerDid !== value.attestedEnforcerBinding.nodeAudience || Date.parse(value.attestedEnforcerBinding.expiresAt) < Date.parse(value.expiry)) {
24777
24819
  ctx.addIssue({ code: external_exports3.ZodIssueCode.custom, path: ["attestedEnforcerBinding"], message: "enforcer binding does not cover the target and share lifetime" });
24778
24820
  }
24779
- if (value.policy.contentSource.shareId !== value.contentSource.shareId || value.policy.contentSource.kvResource !== value.contentSource.kvResource || value.policy.contentSource.selector !== value.contentSource.selector || value.policy.contentSource.encryptionNetwork !== value.encryptionNetwork || value.policy.contentSource.encryptedSymmetricKeyDigestHex !== value.contentSource.encryptedSymmetricKeyDigestHex || value.policy.contentSource.keyVersion !== value.contentSource.keyVersion || value.policy.contentSource.mode !== value.contentSource.mode || value.policy.contentSource.initialCiphertextDigestHex !== value.contentSource.initialCiphertextDigestHex) {
24821
+ const encryptionKeyPath = value.contentSource.encryptionKeyPath;
24822
+ if (encryptionKeyPath !== void 0 && encryptionKeyPath !== `share/${value.contentSource.shareId}`) {
24823
+ ctx.addIssue({ code: external_exports3.ZodIssueCode.custom, path: ["contentSource", "encryptionKeyPath"], message: "share key path must name this share" });
24824
+ }
24825
+ if (value.policy.contentSource.shareId !== value.contentSource.shareId || value.policy.contentSource.kvResource !== value.contentSource.kvResource || value.policy.contentSource.selector !== value.contentSource.selector || value.policy.contentSource.encryptionNetwork !== value.encryptionNetwork || value.policy.contentSource.encryptionKeyPath !== encryptionKeyPath || value.policy.contentSource.encryptedSymmetricKeyDigestHex !== value.contentSource.encryptedSymmetricKeyDigestHex || value.policy.contentSource.keyVersion !== value.contentSource.keyVersion || value.policy.contentSource.mode !== value.contentSource.mode || value.policy.contentSource.initialCiphertextDigestHex !== value.contentSource.initialCiphertextDigestHex) {
24780
24826
  ctx.addIssue({ code: external_exports3.ZodIssueCode.custom, path: ["contentSource"], message: "content source is not bound to policy" });
24781
24827
  }
24782
24828
  if (value.policy.signature.suite !== "Ed25519" || value.policy.signature.signerDid !== value.policy.ownerDid) {
24783
24829
  ctx.addIssue({ code: external_exports3.ZodIssueCode.custom, path: ["policy", "signature"], message: "policy must be signed by its owner" });
24784
24830
  }
24785
24831
  const kvCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "kv");
24786
- const decryptCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "encryption" && capability.resource === value.encryptionNetwork && capability.action === "tinycloud.encryption/decrypt");
24832
+ const expectedEncryptionResource = encryptionKeyPath === void 0 ? value.encryptionNetwork : `${value.encryptionNetwork}/${encryptionKeyPath}`;
24833
+ const decryptCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "encryption" && capability.resource === expectedEncryptionResource && capability.action === "tinycloud.encryption/decrypt");
24787
24834
  if (value.policy.capabilityCeiling.length !== 2 || kvCapabilities.length !== 1 || decryptCapabilities.length !== 1 || kvCapabilities[0]?.resource !== value.contentSource.kvResource || kvCapabilities[0]?.selector !== value.contentSource.selector) {
24788
- ctx.addIssue({ code: external_exports3.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain exact decrypt network" });
24835
+ ctx.addIssue({ code: external_exports3.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain the exact decrypt resource" });
24789
24836
  }
24790
24837
  }
24791
24838
  var unsignedShareEnvelopeV3Schema2 = unsignedShareEnvelopeV3BaseSchema2.superRefine(validateV3Invariants2);
@@ -24795,6 +24842,7 @@ var SEALED_BLOB_VERSION2 = 1;
24795
24842
  var AAD3 = utf8Bytes3(ENVELOPE_AAD_LABEL3);
24796
24843
  var KEY_LENGTH2 = 32;
24797
24844
  var NONCE_LENGTH2 = 12;
24845
+ var TAG_LENGTH2 = 16;
24798
24846
  var HEADER_LENGTH2 = 1;
24799
24847
  function assertKey3(key32) {
24800
24848
  if (key32.length !== KEY_LENGTH2) {
@@ -24822,6 +24870,17 @@ async function seal2(plaintextBytes, key32) {
24822
24870
  blob.set(ciphertext, HEADER_LENGTH2 + nonce.length);
24823
24871
  return { blob, cid: await computeCid2(blob) };
24824
24872
  }
24873
+ async function open7(blob, key32) {
24874
+ if (blob.length < HEADER_LENGTH2 + NONCE_LENGTH2 + TAG_LENGTH2) {
24875
+ throw new TypeError(`sealed blob too short: ${blob.length} bytes`);
24876
+ }
24877
+ if (blob[0] !== SEALED_BLOB_VERSION2) {
24878
+ throw new TypeError(`unknown sealed blob version: ${blob[0]}`);
24879
+ }
24880
+ const nonce = blob.subarray(HEADER_LENGTH2, HEADER_LENGTH2 + NONCE_LENGTH2);
24881
+ const ciphertext = blob.subarray(HEADER_LENGTH2 + NONCE_LENGTH2);
24882
+ return decryptEnvelope2(nonce, ciphertext, key32);
24883
+ }
24825
24884
  async function encryptEnvelope2(plaintextBytes, key32) {
24826
24885
  const key = await importAesKey2(key32, "encrypt");
24827
24886
  const nonce = globalThis.crypto.getRandomValues(new Uint8Array(NONCE_LENGTH2));
@@ -24834,6 +24893,19 @@ async function encryptEnvelope2(plaintextBytes, key32) {
24834
24893
  );
24835
24894
  return { nonce, ciphertext };
24836
24895
  }
24896
+ async function decryptEnvelope2(nonce, ciphertext, key32) {
24897
+ if (nonce.length !== NONCE_LENGTH2) {
24898
+ throw new TypeError(`nonce must be ${NONCE_LENGTH2} bytes, got ${nonce.length}`);
24899
+ }
24900
+ const key = await importAesKey2(key32, "decrypt");
24901
+ return new Uint8Array(
24902
+ await globalThis.crypto.subtle.decrypt(
24903
+ { name: "AES-GCM", iv: nonce, additionalData: AAD3 },
24904
+ key,
24905
+ ciphertext
24906
+ )
24907
+ );
24908
+ }
24837
24909
  function isPlainRecord3(value) {
24838
24910
  const proto = Object.getPrototypeOf(value);
24839
24911
  return proto === Object.prototype || proto === null;
@@ -24948,6 +25020,7 @@ function equal(left, right) {
24948
25020
  return left.length === right.length && left.every((byte, index) => byte === right[index]);
24949
25021
  }
24950
25022
  var ED25519_VERIFY_OPTS2 = { zip215: false };
25023
+ var ENVELOPE_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v2\0";
24951
25024
  var ENVELOPE_V3_SIGNATURE_DOMAIN = "xyz.tinycloud.share/envelope/v3\0";
24952
25025
  var POLICY_V1_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v1\0";
24953
25026
  var POLICY_V2_SIGNATURE_DOMAIN = "xyz.tinycloud.policy/policy/v2\0";
@@ -24955,6 +25028,14 @@ var CONTENT_SOURCE_V1_DOMAIN = "xyz.tinycloud.policy/ContentSource/v1\0";
24955
25028
  var POLICY_CAPABILITY_V1_DOMAIN = "xyz.tinycloud.policy/PolicyCapability/v1\0";
24956
25029
  var NATIVE_PROJECTION_V1_DOMAIN = "xyz.tinycloud.policy/NativeProjection/v1\0";
24957
25030
  var ATTESTED_ENFORCER_V2_DOMAIN = "xyz.tinycloud.policy/AttestedEnforcerBinding/v2\0";
25031
+ function signingBytesV2(unsigned) {
25032
+ const domain = utf8Bytes3(ENVELOPE_V2_SIGNATURE_DOMAIN);
25033
+ const body = utf8Bytes3(canonicalize3(unsigned));
25034
+ const bytes3 = new Uint8Array(domain.length + body.length);
25035
+ bytes3.set(domain);
25036
+ bytes3.set(body, domain.length);
25037
+ return bytes3;
25038
+ }
24958
25039
  function signingBytesV3(unsigned) {
24959
25040
  const domain = utf8Bytes3(ENVELOPE_V3_SIGNATURE_DOMAIN);
24960
25041
  const body = utf8Bytes3(canonicalize3(unsigned));
@@ -24963,6 +25044,17 @@ function signingBytesV3(unsigned) {
24963
25044
  bytes3.set(body, domain.length);
24964
25045
  return bytes3;
24965
25046
  }
25047
+ function verifyEnvelopeV2SignatureOnly(envelope) {
25048
+ const parsed = shareEnvelopeV2Schema2.parse(envelope);
25049
+ const { signature, ...unsigned } = parsed;
25050
+ return ed25519.verify(fromBase64Url3(signature.value), signingBytesV2(unsigned), ed25519PublicKeyFromDidKey2(signature.signerDid), ED25519_VERIFY_OPTS2);
25051
+ }
25052
+ async function verifyEnvelopeV2(envelope, options) {
25053
+ const parsed = shareEnvelopeV2Schema2.parse(envelope);
25054
+ if (parsed.signature.signerDid !== options.expectedSignerDid || !verifyEnvelopeV2SignatureOnly(parsed)) return false;
25055
+ if (parsed.authorizationTarget.kind === "policy") return await computeCid2(fromBase64Url3(parsed.authorizationTarget.policyBytes)) === parsed.authorizationTarget.policyCid;
25056
+ return true;
25057
+ }
24966
25058
  function verifyEnvelopeV3SignatureOnly(envelope) {
24967
25059
  const parsed = shareEnvelopeV3Schema2.parse(envelope);
24968
25060
  const { signature, ...unsigned } = parsed;
@@ -25049,6 +25141,52 @@ function base32Lower(bytes3) {
25049
25141
  if (bits > 0) output += alphabet[buffer << 5 - bits & 31];
25050
25142
  return output;
25051
25143
  }
25144
+ var KEY_LENGTH22 = 32;
25145
+ function assertCanonicalCid(cidString) {
25146
+ const cid2 = CID3.parse(cidString);
25147
+ if (cid2.version !== 1 || cid2.code !== code3 || cid2.multihash.code !== 18 || // sha2-256 (0x12) only, at the link layer too
25148
+ cid2.toString() !== cidString) {
25149
+ throw new TypeError(`not a canonical CIDv1 raw sha2-256 base32 CID: ${cidString}`);
25150
+ }
25151
+ }
25152
+ function parseShareUrl(url, options = {}) {
25153
+ const parsed = new URL(url);
25154
+ if (parsed.protocol !== "https:") {
25155
+ throw new TypeError(`share URL must be https, got ${parsed.protocol}`);
25156
+ }
25157
+ if (parsed.username !== "" || parsed.password !== "") {
25158
+ throw new TypeError("share URL must not carry userinfo");
25159
+ }
25160
+ if (parsed.search !== "") {
25161
+ throw new TypeError("share URL must not have a query string");
25162
+ }
25163
+ if (options.expectedOrigin !== void 0) {
25164
+ if (!isCanonicalHttpsOrigin2(options.expectedOrigin)) {
25165
+ throw new TypeError(
25166
+ `expectedOrigin must be a canonical https origin, got ${options.expectedOrigin}`
25167
+ );
25168
+ }
25169
+ if (parsed.origin !== options.expectedOrigin) {
25170
+ throw new TypeError(
25171
+ `share URL origin ${parsed.origin} does not match expected ${options.expectedOrigin}`
25172
+ );
25173
+ }
25174
+ }
25175
+ const match = /^\/s\/([a-z2-7]+)$/.exec(parsed.pathname);
25176
+ if (!match || match[1] === void 0) {
25177
+ throw new TypeError(`not a share URL path: ${parsed.pathname}`);
25178
+ }
25179
+ const ciphertextCid = match[1];
25180
+ assertCanonicalCid(ciphertextCid);
25181
+ if (!parsed.hash.startsWith("#k=")) {
25182
+ throw new TypeError("share URL is missing the #k= key fragment");
25183
+ }
25184
+ const key32 = fromBase64Url3(parsed.hash.slice("#k=".length));
25185
+ if (key32.length !== KEY_LENGTH22) {
25186
+ throw new TypeError(`fragment key must be ${KEY_LENGTH22} bytes, got ${key32.length}`);
25187
+ }
25188
+ return { ciphertextCid, key32 };
25189
+ }
25052
25190
  var ARTIFACT_ROOT2 = "artifacts";
25053
25191
  function parseArtifactVersionPath2(path) {
25054
25192
  const match = /^artifacts\/([a-z0-9][a-z0-9_-]{2,62})\/versions\/([1-9][0-9]{0,14})\/([^/]+)$/.exec(path);
@@ -25125,6 +25263,7 @@ function verifyArtifactShare(envelope, options) {
25125
25263
  }
25126
25264
  var SHARE_RESULT_VERSION = 1;
25127
25265
  var DEFAULT_MAX_SEALED_BLOB_BYTES = 100 * 1024 * 1024 + 29;
25266
+ var DEFAULT_MAX_CONTENT_BLOB_BYTES = DEFAULT_MAX_SEALED_BLOB_BYTES;
25128
25267
  var ShareReceiveError = class extends Error {
25129
25268
  code;
25130
25269
  details;
@@ -25139,7 +25278,7 @@ var ShareReceiveError = class extends Error {
25139
25278
  }
25140
25279
  };
25141
25280
  function metadataFor(envelope, origin) {
25142
- const kind = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : "emailDomain";
25281
+ const kind = envelope.recipientMatcher.kind === "recipientDid" ? "recipientDid" : envelope.recipientMatcher.kind === "exactEmail" ? "email" : envelope.recipientMatcher.kind === "emailDomain" ? "emailDomain" : "bearer";
25143
25282
  return {
25144
25283
  protocol: "tinycloud-share",
25145
25284
  version: 1,
@@ -25154,7 +25293,7 @@ function metadataFor(envelope, origin) {
25154
25293
  ...envelope.display.filename === void 0 ? {} : { filename: envelope.display.filename },
25155
25294
  ...envelope.display.mode === void 0 ? {} : { mode: envelope.display.mode }
25156
25295
  },
25157
- ...envelope.artifactPin === void 0 ? {} : { artifact: { id: envelope.artifactPin.artifactId, version: envelope.artifactPin.version } }
25296
+ ...envelope.version === 3 && envelope.artifactPin !== void 0 ? { artifact: { id: envelope.artifactPin.artifactId, version: envelope.artifactPin.version } } : {}
25158
25297
  };
25159
25298
  }
25160
25299
  function receiveErrorFor(error) {
@@ -25252,14 +25391,165 @@ async function resolvePolicyShare(link4, options) {
25252
25391
  options.onResolvedAddressedEnvelope?.(envelope, cid2);
25253
25392
  return { envelope, origin: url.origin, cid: cid2 };
25254
25393
  }
25394
+ async function resolveLegacyV2(link4, options) {
25395
+ let parsed;
25396
+ try {
25397
+ parsed = parseShareUrl(link4, { ...options.expectedOrigin === void 0 ? {} : { expectedOrigin: options.expectedOrigin } });
25398
+ } catch {
25399
+ throw new ShareReceiveError("invalid-link", "legacy share link format is invalid");
25400
+ }
25401
+ try {
25402
+ options.signal?.throwIfAborted();
25403
+ const origin = new URL(link4).origin;
25404
+ const limit = options.maxSealedBlobBytes ?? DEFAULT_MAX_SEALED_BLOB_BYTES;
25405
+ if (!Number.isSafeInteger(limit) || limit <= 0) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope limit is invalid");
25406
+ let sealed;
25407
+ if (options.fetchBlob !== void 0) {
25408
+ try {
25409
+ sealed = await options.fetchBlob({ origin, cid: parsed.ciphertextCid, ...options.signal === void 0 ? {} : { signal: options.signal } });
25410
+ } catch {
25411
+ throw new ShareReceiveError("fetch-failed", "legacy registry unavailable");
25412
+ }
25413
+ } else {
25414
+ if (options.registryBaseUrl === void 0) throw new ShareReceiveError("fetch-failed", "legacy registry is required");
25415
+ const response = await (options.fetch ?? globalThis.fetch)(`${options.registryBaseUrl.replace(/\/+$/, "")}/ipfs/${parsed.ciphertextCid}?format=raw`, {
25416
+ headers: { accept: "application/vnd.ipld.raw" },
25417
+ redirect: "error",
25418
+ ...options.signal === void 0 ? {} : { signal: options.signal }
25419
+ });
25420
+ if (!response.ok) throw new ShareReceiveError("fetch-failed", "legacy registry unavailable");
25421
+ if (Number(response.headers.get("content-length")) > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
25422
+ const reader = response.body?.getReader();
25423
+ if (reader === void 0) throw new ShareReceiveError("fetch-failed", "legacy registry returned no body");
25424
+ const chunks = [];
25425
+ let length32 = 0;
25426
+ for (; ; ) {
25427
+ const part = await reader.read();
25428
+ if (part.done) break;
25429
+ length32 += part.value.length;
25430
+ if (length32 > limit) {
25431
+ await reader.cancel();
25432
+ throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
25433
+ }
25434
+ chunks.push(part.value);
25435
+ }
25436
+ sealed = new Uint8Array(length32);
25437
+ let offset = 0;
25438
+ for (const chunk of chunks) {
25439
+ sealed.set(chunk, offset);
25440
+ offset += chunk.length;
25441
+ }
25442
+ }
25443
+ options.signal?.throwIfAborted();
25444
+ if (!(sealed instanceof Uint8Array) || sealed.length > limit) throw new ShareReceiveError("max-bytes-exceeded", "sealed envelope exceeds the size limit");
25445
+ if (await computeCid2(sealed) !== parsed.ciphertextCid) throw new ShareReceiveError("cid-mismatch", "legacy envelope CID does not match");
25446
+ let plaintext;
25447
+ try {
25448
+ plaintext = await open7(sealed, parsed.key32);
25449
+ } catch {
25450
+ throw new ShareReceiveError("decrypt-failed", "legacy envelope could not be opened");
25451
+ }
25452
+ let envelope;
25453
+ try {
25454
+ const encoded = new TextDecoder("utf-8", { fatal: true }).decode(plaintext);
25455
+ const value = JSON.parse(encoded);
25456
+ if (canonicalize3(value) !== encoded) throw new Error("non-canonical envelope");
25457
+ envelope = shareEnvelopeV2Schema2.parse(value);
25458
+ } catch {
25459
+ throw new ShareReceiveError("envelope-invalid", "legacy envelope is invalid");
25460
+ }
25461
+ return { envelope, origin, cid: parsed.ciphertextCid };
25462
+ } finally {
25463
+ parsed.key32.fill(0);
25464
+ }
25465
+ }
25466
+ function evidenceMatchesV2(envelope, evidence) {
25467
+ const actions = new Set(evidence.actions.map((action) => action === "read" ? "tinycloud.kv/get" : action === "list" ? "tinycloud.kv/list" : action === "edit" ? "tinycloud.kv/put" : action));
25468
+ const resourceCovers = evidence.resource.kind === "exact" ? envelope.resource.kind === "exact" && evidence.resource.path === envelope.resource.path : envelope.resource.path === evidence.resource.path || envelope.resource.path.startsWith(evidence.resource.path.endsWith("/") ? evidence.resource.path : `${evidence.resource.path}/`);
25469
+ return envelope.authorizationTarget.kind === "policy" && evidence.policyCid === envelope.authorizationTarget.policyCid && evidence.signerDid === envelope.signature.signerDid && evidence.registrationCid.length > 0 && evidence.shareId === envelope.shareId && canonicalize3(evidence.recipientMatcher) === canonicalize3(envelope.recipientMatcher) && canonicalize3(evidence.target) === canonicalize3(envelope.target) && resourceCovers && envelope.actions.every((action) => actions.has(action === "read" ? "tinycloud.kv/get" : action === "list" ? "tinycloud.kv/list" : "tinycloud.kv/put")) && canonicalize3(evidence.contentSource) === canonicalize3(envelope.contentSource) && evidence.contentSourceDigest === envelope.contentSourceDigest && evidence.delegationCid === envelope.delegationCid && evidence.authorityMaterialHandle === envelope.authorityMaterialHandle && evidence.authorityMaterialDigest === envelope.authorityMaterialDigest && evidence.expiresAt === envelope.expiry && Number.isFinite(Date.parse(evidence.expiresAt)) && Date.parse(evidence.expiresAt) === Date.parse(envelope.expiry);
25470
+ }
25471
+ async function verifyLegacyV2(envelope, options) {
25472
+ try {
25473
+ if (!await verifyEnvelopeV2(envelope, { expectedSignerDid: envelope.signature.signerDid })) throw new Error("signature");
25474
+ } catch {
25475
+ throw new ShareReceiveError("signature-invalid", "legacy envelope signature is invalid");
25476
+ }
25477
+ const expiry = Date.parse(envelope.expiry);
25478
+ if (!Number.isFinite(expiry)) throw new ShareReceiveError("envelope-invalid", "legacy share expiry is invalid");
25479
+ if (expiry <= (options.now?.() ?? Date.now())) throw new ShareReceiveError("expired", "legacy share has expired", { expiresAt: envelope.expiry });
25480
+ if (envelope.authorizationTarget.kind === "policy") {
25481
+ if (options.trustedPolicyAuthority === void 0) throw new ShareReceiveError("envelope-invalid", "a legacy policy authority is required");
25482
+ let evidence;
25483
+ try {
25484
+ evidence = await options.trustedPolicyAuthority.resolve({ policyCid: envelope.authorizationTarget.policyCid, envelope });
25485
+ } catch {
25486
+ }
25487
+ if (evidence === void 0 || !evidenceMatchesV2(envelope, evidence)) throw new ShareReceiveError("capability-invalid", "legacy policy evidence does not match");
25488
+ if (options.trustedSignerDid !== void 0 && options.trustedSignerDid !== evidence.signerDid) throw new ShareReceiveError("signature-invalid", "legacy signer is not trusted");
25489
+ } else if (options.trustedSignerDid === void 0 || options.trustedSignerDid !== envelope.signature.signerDid || envelope.authorizationTarget.kind === "recipientDid" && envelope.recipientMatcher.kind === "recipientDid" && envelope.authorizationTarget.did !== envelope.recipientMatcher.value) {
25490
+ throw new ShareReceiveError("capability-invalid", "legacy recipient authority does not match");
25491
+ }
25492
+ }
25255
25493
  async function inspectShare(link4, options = {}) {
25494
+ if (isLegacyV2Link(link4)) {
25495
+ const legacy = await resolveLegacyV2(link4, options);
25496
+ await verifyLegacyV2(legacy.envelope, options);
25497
+ options.onResolvedAddressedEnvelope?.(legacy.envelope, legacy.cid);
25498
+ return { metadata: metadataFor(legacy.envelope, legacy.origin), link: { origin: legacy.origin, cid: legacy.cid, kind: "compact" } };
25499
+ }
25256
25500
  const resolved = await resolvePolicyShare(link4, options);
25257
25501
  return { metadata: metadataFor(resolved.envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: "policy" } };
25258
25502
  }
25259
25503
  async function receiveShare(link4, options = {}) {
25504
+ if (isLegacyV2Link(link4)) return receiveLegacyV2(link4, options);
25260
25505
  const resolved = await resolvePolicyShare(link4, options);
25261
25506
  return { state: "authorization-required", method: resolved.envelope.recipientMatcher.kind === "recipientDid" ? "openkey-device" : "email-claim" };
25262
25507
  }
25508
+ function isLegacyV2Link(link4) {
25509
+ try {
25510
+ const url = new URL(link4);
25511
+ return /^\/s\/bafkrei[a-z2-7]{52}$/.test(url.pathname) && url.hash.startsWith("#k=");
25512
+ } catch {
25513
+ return false;
25514
+ }
25515
+ }
25516
+ async function receiveLegacyV2(link4, options) {
25517
+ const resolved = await resolveLegacyV2(link4, options);
25518
+ const { envelope } = resolved;
25519
+ await verifyLegacyV2(envelope, options);
25520
+ options.onResolvedAddressedEnvelope?.(envelope, resolved.cid);
25521
+ const method = envelope.recipientMatcher.kind === "recipientDid" ? "openkey-device" : "email-claim";
25522
+ if (options.authorization === void 0) return { state: "authorization-required", method };
25523
+ const result = options.authorizationResumeToken === void 0 ? await options.authorization.begin({ envelope, method }) : await options.authorization.resume({
25524
+ envelope,
25525
+ method,
25526
+ resumeToken: options.authorizationResumeToken,
25527
+ ...options.authorizationProof === void 0 ? {} : { proof: options.authorizationProof }
25528
+ });
25529
+ if (result.state === "authorization-required") return result;
25530
+ if (result.state === "denied") throw new ShareReceiveError("authorization-denied", "legacy share authorization was denied");
25531
+ if (result.value === null || typeof result.value !== "object" || !(result.value.bytes instanceof Uint8Array) || result.value.proof === void 0 || options.authorization.verifyResult === void 0) {
25532
+ throw new ShareReceiveError("content-integrity-failed", "legacy content lacks a verifiable proof");
25533
+ }
25534
+ const authorized = result.value;
25535
+ try {
25536
+ if (!await options.authorization.verifyResult({ envelope, value: authorized, proof: authorized.proof })) throw new Error("proof");
25537
+ } catch {
25538
+ throw new ShareReceiveError("content-integrity-failed", "legacy content proof is invalid");
25539
+ }
25540
+ const bytes3 = authorized.bytes.slice();
25541
+ if (bytes3.length > (options.maxContentBlobBytes ?? DEFAULT_MAX_CONTENT_BLOB_BYTES)) throw new ShareReceiveError("max-bytes-exceeded", "legacy content exceeds the size limit");
25542
+ const bodyDigest = toBase64Url2(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes3)));
25543
+ if (authorized.bodyDigest !== bodyDigest || authorized.contentSourceDigest !== envelope.contentSourceDigest || authorized.binding.shareId !== envelope.shareId || authorized.binding.delegationCid !== envelope.delegationCid || authorized.binding.authorityMaterialHandle !== envelope.authorityMaterialHandle || authorized.binding.authorityMaterialDigest !== envelope.authorityMaterialDigest || authorized.binding.resource.kind !== envelope.resource.kind || authorized.binding.resource.path !== envelope.resource.path || authorized.binding.action !== void 0 && !envelope.actions.some((action) => action === authorized.binding.action || `tinycloud.kv/${action}` === authorized.binding.action) || envelope.metadata.byteLength !== void 0 && envelope.metadata.byteLength !== bytes3.length) {
25544
+ throw new ShareReceiveError("content-integrity-failed", "legacy content binding does not match");
25545
+ }
25546
+ let text;
25547
+ try {
25548
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes3);
25549
+ } catch {
25550
+ }
25551
+ return { metadata: metadataFor(envelope, resolved.origin), link: { origin: resolved.origin, cid: resolved.cid, kind: "compact" }, bytes: bytes3, ...text === void 0 ? {} : { text } };
25552
+ }
25263
25553
  var SHARE_CONTENT_LIMIT = 100 * 1024 * 1024;
25264
25554
  var SHARE_PUBLISH_RESULT_VERSION = 1;
25265
25555
  var DEFAULT_SHARE_LIFETIME_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -25747,7 +26037,10 @@ async function publishTargetShare(input) {
25747
26037
  expiresAt,
25748
26038
  expiryWasExplicit: input.expiryWasExplicit ?? input.expiresAt !== void 0,
25749
26039
  origin: input.origin,
25750
- ...input.notify === void 0 ? {} : { notify: input.notify }
26040
+ ...input.notify === void 0 ? {} : { notify: input.notify },
26041
+ ...input.credentialDescriptor === void 0 ? {} : { credentialDescriptor: input.credentialDescriptor },
26042
+ ...input.issuerDid === void 0 ? {} : { issuerDid: input.issuerDid },
26043
+ ...input.issuerKid === void 0 ? {} : { issuerKid: input.issuerKid }
25751
26044
  });
25752
26045
  }
25753
26046
  var POLICY_V1_DOMAIN = "xyz.tinycloud.policy/policy/v1\0";
@@ -25833,7 +26126,7 @@ async function createPolicy(options, capabilities) {
25833
26126
  function addressedPlan(options) {
25834
26127
  const capabilities = sortCanonical([
25835
26128
  { kind: "kv", resource: options.contentSource.kvResource, selector: options.resource.kind, actions: [...options.policyActions] },
25836
- { kind: "encryption", resource: options.contentSource.encryptionNetwork, action: "tinycloud.encryption/decrypt" }
26129
+ { kind: "encryption", resource: options.contentSource.encryptionKeyPath === void 0 ? options.contentSource.encryptionNetwork : `${options.contentSource.encryptionNetwork}/${options.contentSource.encryptionKeyPath}`, action: "tinycloud.encryption/decrypt" }
25837
26130
  ]);
25838
26131
  return {
25839
26132
  expiry: rfc3339Seconds(options.expiresAt),
@@ -26158,6 +26451,7 @@ async function addressedShareBodyUpperBound(input) {
26158
26451
  kvResource: `${input.spaceId}/kv/${path}`,
26159
26452
  selector: input.resource.kind,
26160
26453
  encryptionNetwork: input.encryptionNetwork,
26454
+ ...input.encryptionKeyPath === void 0 ? {} : { encryptionKeyPath: input.encryptionKeyPath },
26161
26455
  encryptedSymmetricKeyDigestHex: "f".repeat(64),
26162
26456
  keyVersion: Number.MAX_SAFE_INTEGER,
26163
26457
  mode: "immutable",
@@ -28202,6 +28496,23 @@ var unifiedEncryptionNetworkSchema3 = external_exports2.string().refine((value)
28202
28496
  const network = separator < 0 ? "" : rest.slice(separator + 1);
28203
28497
  return owner.startsWith("did:") && owner.length > 4 && network.length > 0 && !/[:/%?#\s]/.test(network);
28204
28498
  }, { message: "expected canonical TinyCloud encryption network" });
28499
+ var unifiedEncryptionResourceSchema3 = external_exports2.string().refine((value) => {
28500
+ const prefix = "urn:tinycloud:encryption:";
28501
+ if (!value.startsWith(prefix)) return false;
28502
+ const match = /^(did:.+):([^/:?#\s]+)(?:\/(.+))?$/.exec(value.slice(prefix.length));
28503
+ if (match === null) return false;
28504
+ const path = match[3];
28505
+ return path === void 0 || path.split("/").every(
28506
+ (segment) => segment.length > 0 && segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment)
28507
+ );
28508
+ }, { message: "expected a canonical TinyCloud encryption resource" });
28509
+ var unifiedEncryptionKeyPathSchema3 = external_exports2.string().regex(/^share\/[^/]+$/).refine(
28510
+ (value) => {
28511
+ const segment = value.slice("share/".length);
28512
+ return segment !== "." && segment !== ".." && !/[\\:%?#\s]/.test(segment);
28513
+ },
28514
+ { message: "expected an exact share key path" }
28515
+ );
28205
28516
  var unifiedKvCapabilitySchema3 = external_exports2.object({
28206
28517
  kind: external_exports2.literal("kv"),
28207
28518
  resource: unifiedResourceSchema3,
@@ -28215,7 +28526,7 @@ var unifiedKvCapabilitySchema3 = external_exports2.object({
28215
28526
  }).strict();
28216
28527
  var unifiedEncryptionCapabilitySchema3 = external_exports2.object({
28217
28528
  kind: external_exports2.literal("encryption"),
28218
- resource: unifiedEncryptionNetworkSchema3,
28529
+ resource: unifiedEncryptionResourceSchema3,
28219
28530
  action: external_exports2.literal("tinycloud.encryption/decrypt")
28220
28531
  }).strict();
28221
28532
  var unifiedCapabilitySchema3 = external_exports2.union([unifiedKvCapabilitySchema3, unifiedEncryptionCapabilitySchema3]);
@@ -28224,6 +28535,7 @@ var unifiedContentSourceSchema3 = external_exports2.object({
28224
28535
  kvResource: unifiedResourceSchema3,
28225
28536
  selector: external_exports2.union([external_exports2.literal("exact"), external_exports2.literal("prefix")]),
28226
28537
  encryptionNetwork: unifiedEncryptionNetworkSchema3,
28538
+ encryptionKeyPath: unifiedEncryptionKeyPathSchema3.optional(),
28227
28539
  encryptedSymmetricKeyDigestHex: external_exports2.string().regex(/^[0-9a-f]{64}$/),
28228
28540
  keyVersion: external_exports2.number().int().positive(),
28229
28541
  mode: external_exports2.union([external_exports2.literal("mutable"), external_exports2.literal("immutable")]),
@@ -28341,16 +28653,21 @@ function validateV3Invariants3(value, ctx) {
28341
28653
  if (value.attestedEnforcerBinding.nodeAudience !== value.target.nodeAudience || value.attestedEnforcerBinding.signature.signerDid !== value.attestedEnforcerBinding.nodeAudience || Date.parse(value.attestedEnforcerBinding.expiresAt) < Date.parse(value.expiry)) {
28342
28654
  ctx.addIssue({ code: external_exports2.ZodIssueCode.custom, path: ["attestedEnforcerBinding"], message: "enforcer binding does not cover the target and share lifetime" });
28343
28655
  }
28344
- if (value.policy.contentSource.shareId !== value.contentSource.shareId || value.policy.contentSource.kvResource !== value.contentSource.kvResource || value.policy.contentSource.selector !== value.contentSource.selector || value.policy.contentSource.encryptionNetwork !== value.encryptionNetwork || value.policy.contentSource.encryptedSymmetricKeyDigestHex !== value.contentSource.encryptedSymmetricKeyDigestHex || value.policy.contentSource.keyVersion !== value.contentSource.keyVersion || value.policy.contentSource.mode !== value.contentSource.mode || value.policy.contentSource.initialCiphertextDigestHex !== value.contentSource.initialCiphertextDigestHex) {
28656
+ const encryptionKeyPath = value.contentSource.encryptionKeyPath;
28657
+ if (encryptionKeyPath !== void 0 && encryptionKeyPath !== `share/${value.contentSource.shareId}`) {
28658
+ ctx.addIssue({ code: external_exports2.ZodIssueCode.custom, path: ["contentSource", "encryptionKeyPath"], message: "share key path must name this share" });
28659
+ }
28660
+ if (value.policy.contentSource.shareId !== value.contentSource.shareId || value.policy.contentSource.kvResource !== value.contentSource.kvResource || value.policy.contentSource.selector !== value.contentSource.selector || value.policy.contentSource.encryptionNetwork !== value.encryptionNetwork || value.policy.contentSource.encryptionKeyPath !== encryptionKeyPath || value.policy.contentSource.encryptedSymmetricKeyDigestHex !== value.contentSource.encryptedSymmetricKeyDigestHex || value.policy.contentSource.keyVersion !== value.contentSource.keyVersion || value.policy.contentSource.mode !== value.contentSource.mode || value.policy.contentSource.initialCiphertextDigestHex !== value.contentSource.initialCiphertextDigestHex) {
28345
28661
  ctx.addIssue({ code: external_exports2.ZodIssueCode.custom, path: ["contentSource"], message: "content source is not bound to policy" });
28346
28662
  }
28347
28663
  if (value.policy.signature.suite !== "Ed25519" || value.policy.signature.signerDid !== value.policy.ownerDid) {
28348
28664
  ctx.addIssue({ code: external_exports2.ZodIssueCode.custom, path: ["policy", "signature"], message: "policy must be signed by its owner" });
28349
28665
  }
28350
28666
  const kvCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "kv");
28351
- const decryptCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "encryption" && capability.resource === value.encryptionNetwork && capability.action === "tinycloud.encryption/decrypt");
28667
+ const expectedEncryptionResource = encryptionKeyPath === void 0 ? value.encryptionNetwork : `${value.encryptionNetwork}/${encryptionKeyPath}`;
28668
+ const decryptCapabilities = value.policy.capabilityCeiling.filter((capability) => capability.kind === "encryption" && capability.resource === expectedEncryptionResource && capability.action === "tinycloud.encryption/decrypt");
28352
28669
  if (value.policy.capabilityCeiling.length !== 2 || kvCapabilities.length !== 1 || decryptCapabilities.length !== 1 || kvCapabilities[0]?.resource !== value.contentSource.kvResource || kvCapabilities[0]?.selector !== value.contentSource.selector) {
28353
- ctx.addIssue({ code: external_exports2.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain exact decrypt network" });
28670
+ ctx.addIssue({ code: external_exports2.ZodIssueCode.custom, path: ["policy", "capabilityCeiling"], message: "policy ceiling must contain the exact decrypt resource" });
28354
28671
  }
28355
28672
  }
28356
28673
  var unsignedShareEnvelopeV3Schema3 = unsignedShareEnvelopeV3BaseSchema3.superRefine(validateV3Invariants3);
@@ -28557,7 +28874,7 @@ function receiveJson(result, path) {
28557
28874
 
28558
28875
  // src/share/io.ts
28559
28876
  import { constants } from "fs";
28560
- import { lstat as lstat6, mkdir as mkdir4, mkdtemp, open as open7, readFile as readFile6, realpath as realpath2, stat as stat4, link as link2, rename as rename3, rm as rm5, unlink as unlink4 } from "fs/promises";
28877
+ import { lstat as lstat6, mkdir as mkdir4, mkdtemp, open as open8, readFile as readFile6, realpath as realpath2, stat as stat4, link as link2, rename as rename3, rm as rm5, unlink as unlink4 } from "fs/promises";
28561
28878
  import { randomBytes as randomBytes3 } from "crypto";
28562
28879
  import { basename as basename2, join as join11, resolve as resolve4, sep as sep3 } from "path";
28563
28880
  init_errors();
@@ -28633,7 +28950,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
28633
28950
  const outputDirectory = resolve4(directory);
28634
28951
  await assertDirectory(outputDirectory);
28635
28952
  const safeName = safeFilename(filename);
28636
- const directoryHandle = await open7(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
28953
+ const directoryHandle = await open8(outputDirectory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0));
28637
28954
  const stableDirectory = await realpath2(outputDirectory);
28638
28955
  const outputPath = join11(stableDirectory, safeName);
28639
28956
  const directoryIdentity = await directoryHandle.stat();
@@ -28657,7 +28974,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
28657
28974
  if (error.code !== "ENOENT") throw error;
28658
28975
  }
28659
28976
  temporaryPath = stagingPath;
28660
- handle = await open7(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
28977
+ handle = await open8(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
28661
28978
  await handle.writeFile(bytes);
28662
28979
  await handle.close();
28663
28980
  handle = void 0;
@@ -28807,6 +29124,38 @@ function createShareLinks(input) {
28807
29124
  };
28808
29125
  }
28809
29126
 
29127
+ // src/share/email-publish-v3.ts
29128
+ import {
29129
+ canonicalDigest,
29130
+ createEmailCredentialRequirement,
29131
+ credentialRequirementDigest,
29132
+ validateCredentialFlowDescriptor
29133
+ } from "@tinycloud/sdk-core";
29134
+ async function publishEmailShareV3(input) {
29135
+ const { contentSource } = input;
29136
+ if (input.resource.kind !== "exact" || contentSource.encryptionKeyPath !== `share/${input.shareId}` || !/^urn:tinycloud:encryption:did:pkh:eip155:\d+:0x[0-9a-fA-F]{40}:default$/.test(contentSource.encryptionNetwork)) {
29137
+ throw new TypeError("email share binding is invalid");
29138
+ }
29139
+ const descriptor = validateCredentialFlowDescriptor(input.credentialDescriptor);
29140
+ if (descriptor.issuer.did !== input.issuerDid || descriptor.issuer.kid !== input.issuerKid || !descriptor.claims.some((claim) => claim.name === "email" && claim.matching === "normalized_exact")) {
29141
+ throw new TypeError("email credential issuer or claim is invalid");
29142
+ }
29143
+ const profile = { id: descriptor.profile, version: 1 };
29144
+ const credentialType = { id: descriptor.format.vct, version: 1 };
29145
+ const requirement = createEmailCredentialRequirement({ email: input.target.address, profile, credentialType });
29146
+ const credentialRequirement = {
29147
+ type: "TinyCloudPolicyCredentialRequirement",
29148
+ version: 1,
29149
+ requirementDigest: await credentialRequirementDigest(requirement),
29150
+ descriptorDigest: await canonicalDigest(descriptor),
29151
+ issuerDid: input.issuerDid,
29152
+ issuerKid: input.issuerKid,
29153
+ profile,
29154
+ credentialType
29155
+ };
29156
+ return publishAddressedShare({ ...input, credentialRequirement });
29157
+ }
29158
+
28810
29159
  // src/share/errors.ts
28811
29160
  var SharePublishAuthorityError = class extends Error {
28812
29161
  constructor(failure, options) {
@@ -28868,7 +29217,7 @@ init_profiles();
28868
29217
  init_constants();
28869
29218
  init_errors();
28870
29219
  init_session_expired();
28871
- import { chmod as chmod4, link as link3, open as open8, readFile as readFile7, stat as stat5, unlink as unlink5 } from "fs/promises";
29220
+ import { chmod as chmod4, link as link3, open as open9, readFile as readFile7, stat as stat5, unlink as unlink5 } from "fs/promises";
28872
29221
  import { dirname as dirname5, join as join12 } from "path";
28873
29222
  import { createHash } from "crypto";
28874
29223
  import { writeJsonAtomic as writeJsonAtomic2 } from "@tinycloud/operations/state";
@@ -29225,7 +29574,7 @@ function createEncryptedProfileHistory(profileName, sessionSigner) {
29225
29574
  await link3(found.file, target);
29226
29575
  await chmod4(target, PRIVATE_HISTORY_MODE);
29227
29576
  await unlink5(found.file);
29228
- const directory = await open8(dirname5(found.file), "r");
29577
+ const directory = await open9(dirname5(found.file), "r");
29229
29578
  try {
29230
29579
  await directory.sync();
29231
29580
  } finally {
@@ -29428,8 +29777,15 @@ function createShareAuthorityAdapters(input = {}) {
29428
29777
  profileName: activeProfileName2
29429
29778
  });
29430
29779
  }
29431
- const addressedEncryptionNetwork = targetInput.target.kind === "bearer" ? void 0 : node.getEncryptionNetworkIdForSpace(ownerSpaceId);
29432
- if (addressedEncryptionNetwork !== void 0 && node.isSessionOnly && !sessionHoldsEncryptionDecrypt(node, addressedEncryptionNetwork)) {
29780
+ if (targetInput.target.kind === "email" && (targetInput.credentialDescriptor === void 0 || targetInput.issuerDid === void 0 || targetInput.issuerKid === void 0)) {
29781
+ throw new TypeError("email credential descriptor and issuer are required");
29782
+ }
29783
+ const addressedEncryptionNetwork = targetInput.target.kind === "bearer" ? void 0 : targetInput.target.kind === "email" ? node.getDefaultEncryptionNetworkId() : node.getEncryptionNetworkIdForSpace(ownerSpaceId);
29784
+ const emailKeyPath = targetInput.target.kind === "email" ? `share/${crypto.randomUUID().replaceAll("-", "")}` : void 0;
29785
+ if (addressedEncryptionNetwork !== void 0 && node.isSessionOnly && !sessionHoldsEncryptionDecrypt(
29786
+ node,
29787
+ emailKeyPath === void 0 ? addressedEncryptionNetwork : `${addressedEncryptionNetwork}/${emailKeyPath}`
29788
+ )) {
29433
29789
  throw new SharePublishAuthorityError({ kind: "decrypt-not-held" });
29434
29790
  }
29435
29791
  const config = await publicConfig();
@@ -29438,7 +29794,7 @@ function createShareAuthorityAdapters(input = {}) {
29438
29794
  if (targetInput.notify === true && targetInput.target.kind === "emailDomain") {
29439
29795
  await assertDomainDeliveryForOrigin(activeNode.origin, "publish");
29440
29796
  }
29441
- const shareId = crypto.randomUUID().replaceAll("-", "");
29797
+ const shareId = emailKeyPath?.slice("share/".length) ?? crypto.randomUUID().replaceAll("-", "");
29442
29798
  const files = targetInput.files === void 0 || targetInput.files.length === 0 ? [{ bytes: targetInput.source, filename: targetInput.filename, mediaType: targetInput.mediaType }] : targetInput.files;
29443
29799
  const resourceKind = targetInput.resourceKind ?? "exact";
29444
29800
  if (targetInput.target.kind === "bearer") {
@@ -29560,6 +29916,7 @@ function createShareAuthorityAdapters(input = {}) {
29560
29916
  resource: { kind: resourceKind },
29561
29917
  storage: artifact === void 0 ? { path: resourcePath } : { artifactId: artifact.id, storageName },
29562
29918
  encryptionNetwork: addressedEncryptionNetwork,
29919
+ ...emailKeyPath === void 0 ? {} : { encryptionKeyPath: emailKeyPath },
29563
29920
  actions,
29564
29921
  policyActions,
29565
29922
  ...deliveryEmail === void 0 ? {} : { deliveryEmail },
@@ -29575,8 +29932,14 @@ function createShareAuthorityAdapters(input = {}) {
29575
29932
  throw new SharePublishAuthorityError({ kind: rejected ? "registry-rejected" : "registry-unavailable" });
29576
29933
  }
29577
29934
  const encryptionNetwork = addressedEncryptionNetwork;
29578
- const encrypted = await node.encryption.encryptToNetwork(encryptionNetwork, file.bytes, { metadata: { contentType: mediaType } });
29935
+ const encrypted = await node.encryption.encryptToNetwork(encryptionNetwork, file.bytes, {
29936
+ ...emailKeyPath === void 0 ? {} : { keyPath: emailKeyPath },
29937
+ metadata: { contentType: mediaType }
29938
+ });
29579
29939
  if (!encrypted.ok) throw new Error("addressed source encryption was rejected");
29940
+ if (emailKeyPath !== void 0 && (encrypted.data.networkId !== encryptionNetwork || encrypted.data.keyPath !== emailKeyPath)) {
29941
+ throw new Error("email share encryption was rejected");
29942
+ }
29580
29943
  const storedBytes = new TextEncoder().encode(canonicalize4(encrypted.data));
29581
29944
  let artifactVersion;
29582
29945
  if (artifact !== void 0) {
@@ -29609,6 +29972,7 @@ function createShareAuthorityAdapters(input = {}) {
29609
29972
  kvResource: `${ownerSpaceId}/kv/${resourcePath}`,
29610
29973
  selector: resourceKind,
29611
29974
  encryptionNetwork: encrypted.data.networkId,
29975
+ ...emailKeyPath === void 0 ? {} : { encryptionKeyPath: emailKeyPath },
29612
29976
  encryptedSymmetricKeyDigestHex: encrypted.data.encryptedSymmetricKeyHash,
29613
29977
  keyVersion: encrypted.data.keyVersion,
29614
29978
  mode: "immutable",
@@ -29650,7 +30014,13 @@ function createShareAuthorityAdapters(input = {}) {
29650
30014
  };
29651
30015
  let published;
29652
30016
  try {
29653
- published = await publishAddressedShare(addressedOptions);
30017
+ published = prepared.target.kind === "email" ? await publishEmailShareV3({
30018
+ ...addressedOptions,
30019
+ target: prepared.target,
30020
+ credentialDescriptor: targetInput.credentialDescriptor,
30021
+ issuerDid: targetInput.issuerDid,
30022
+ issuerKid: targetInput.issuerKid
30023
+ }) : await publishAddressedShare(addressedOptions);
29654
30024
  } catch (error) {
29655
30025
  if (!(error instanceof AddressedShareOverLimitError)) throw error;
29656
30026
  const remaining = await removeOverLimitShare({
@@ -29843,7 +30213,7 @@ init_profiles();
29843
30213
 
29844
30214
  // src/share/html-folder.ts
29845
30215
  import { constants as constants2, existsSync } from "fs";
29846
- import { lstat as lstat7, open as open9, readdir as readdir6, realpath as realpath3, stat as stat6 } from "fs/promises";
30216
+ import { lstat as lstat7, open as open10, readdir as readdir6, realpath as realpath3, stat as stat6 } from "fs/promises";
29847
30217
  import { basename as basename3, dirname as dirname6, isAbsolute, join as join13, relative as relativePath, resolve as resolve5, sep as sep4 } from "path";
29848
30218
 
29849
30219
  // ../share-sdk/dist/html-bundle.js
@@ -30198,9 +30568,9 @@ function scanMarkup(html, options) {
30198
30568
  if (comments > MAX_ARTIFACT_COMMENTS) fail("limit", "artifact page has too many comments");
30199
30569
  };
30200
30570
  while (index < length5) {
30201
- const open10 = html.indexOf("<", index);
30202
- if (open10 === -1) return;
30203
- index = open10 + 1;
30571
+ const open11 = html.indexOf("<", index);
30572
+ if (open11 === -1) return;
30573
+ index = open11 + 1;
30204
30574
  const next = html[index] ?? "";
30205
30575
  if (html.startsWith("!--", index)) {
30206
30576
  countComment();
@@ -30801,7 +31171,7 @@ async function readRegularFile(root, path, relative, remaining, limit, hooks = {
30801
31171
  await hooks.beforeOpen?.();
30802
31172
  before = await lstat7(path);
30803
31173
  await hooks.afterLstat?.();
30804
- handle = await open9(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0) | (constants2.O_NONBLOCK ?? 0));
31174
+ handle = await open10(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0) | (constants2.O_NONBLOCK ?? 0));
30805
31175
  } catch (error) {
30806
31176
  if (error.code === "ELOOP") return problem("symbolic link");
30807
31177
  if (isRaceError(error)) return problem(CHANGED);
@@ -30860,7 +31230,7 @@ async function listFolderDirectory(root, absolute, relative, hooks = {}) {
30860
31230
  if (before.isSymbolicLink()) return problem("symbolic link");
30861
31231
  if (!before.isDirectory()) return problem(CHANGED);
30862
31232
  await hooks.afterLstat?.();
30863
- const handle = await open9(absolute, constants2.O_RDONLY | (constants2.O_DIRECTORY ?? 0) | (constants2.O_NOFOLLOW ?? 0));
31233
+ const handle = await open10(absolute, constants2.O_RDONLY | (constants2.O_DIRECTORY ?? 0) | (constants2.O_NOFOLLOW ?? 0));
30864
31234
  try {
30865
31235
  await hooks.afterOpen?.();
30866
31236
  const info = await handle.stat();
@@ -31287,7 +31657,7 @@ function assertAggregateInputLimit(inputs, maxBytes) {
31287
31657
  }
31288
31658
  function registerShareCommand(program2) {
31289
31659
  const share = program2.command("share").description("Publish and consume TinyCloud Share links");
31290
- share.command("publish <files...>").description("Publish one or more bounded files as a Share, or a folder with an index.html as one self-contained page (anyone links only)").option("--name <filename>", "Filename for stdin input, or the published .html name of a folder").option("--to <target>", "Share target", "anyone").option("--notify", "Email an exact-email recipient, or a domain mailbox on node 1.17.3+").option("--notify-to <address>", "Mailbox to invite for --to domain:<name> --notify (node 1.17.3+)").option("--expires <duration>", "Share lifetime").option("--max-bytes <bytes>", "Bound input bytes").option("--media-type <type>", "Media type for a single input").option("--action <actions...>", "Addressed permission: read, list, or edit").option("--prefix", "Publish multiple inputs beneath one addressed prefix").option("--binary", "Allow non-UTF-8 bearer content").option("--artifact <id>", 'Publish as the next write-once version of an artifact ("new" starts one); the share is pinned to that version').addOption(new Option("--ongoing").hideHelp()).option("--note <text>", "Note stored with the artifact version").option("--json", "Print versioned redacted JSON").option("--full-link", "Print the full self-contained link instead of a short link (addressed shares)").option("--viewer-origin <origin>", "Canonical HTTPS viewer origin", SHARE_ORIGIN).action(async (files, options, command) => {
31660
+ share.command("publish <files...>").description("Publish one or more bounded files as a Share, or a folder with an index.html as one self-contained page (anyone links only)").option("--name <filename>", "Filename for stdin input, or the published .html name of a folder").option("--to <target>", "Share target", "anyone").option("--credential-descriptor <path>", "Email credential flow descriptor JSON").option("--issuer-did <did>", "Trusted email credential issuer DID").option("--issuer-kid <kid>", "Trusted email credential issuer key ID").option("--notify", "Email an exact-email recipient, or a domain mailbox on node 1.17.3+").option("--notify-to <address>", "Mailbox to invite for --to domain:<name> --notify (node 1.17.3+)").option("--expires <duration>", "Share lifetime").option("--max-bytes <bytes>", "Bound input bytes").option("--media-type <type>", "Media type for a single input").option("--action <actions...>", "Addressed permission: read, list, or edit").option("--prefix", "Publish multiple inputs beneath one addressed prefix").option("--binary", "Allow non-UTF-8 bearer content").option("--artifact <id>", 'Publish as the next write-once version of an artifact ("new" starts one); the share is pinned to that version').addOption(new Option("--ongoing").hideHelp()).option("--note <text>", "Note stored with the artifact version").option("--json", "Print versioned redacted JSON").option("--full-link", "Print the full self-contained link instead of a short link (addressed shares)").option("--viewer-origin <origin>", "Canonical HTTPS viewer origin", SHARE_ORIGIN).action(async (files, options, command) => {
31291
31661
  try {
31292
31662
  const json = jsonOutput(options, command);
31293
31663
  if (options.ongoing === true) throw new CLIError("UNSUPPORTED_TARGET", ONGOING_UNAVAILABLE, 2);
@@ -31295,6 +31665,15 @@ function registerShareCommand(program2) {
31295
31665
  if (files.length === 0 || files.includes("-") && files.length > 1) throw new CLIError("INVALID_ARGUMENT", "stdin must be the only publish input", 2);
31296
31666
  const name = files.length === 1 ? options.name : void 0;
31297
31667
  const target = parseShareTarget(options.to);
31668
+ const actions = requestedActions(options.action);
31669
+ if (options.notify === true && !actions.includes("read")) throw new CLIError("INVALID_ARGUMENT", "--notify requires the read action; nothing was shared", 2);
31670
+ if (target.kind === "email" && (!options.credentialDescriptor || !options.issuerDid || !options.issuerKid)) {
31671
+ throw new CLIError("INVALID_ARGUMENT", "email publication requires --credential-descriptor, --issuer-did, and --issuer-kid", 2);
31672
+ }
31673
+ if (target.kind !== "email" && (options.credentialDescriptor || options.issuerDid || options.issuerKid)) {
31674
+ throw new CLIError("INVALID_ARGUMENT", "credential options require an exact email target", 2);
31675
+ }
31676
+ const credentialDescriptor = target.kind === "email" ? JSON.parse(await readFile8(options.credentialDescriptor, "utf8")) : void 0;
31298
31677
  const folder = files.length === 1 && target.kind === "bearer" && options.prefix !== true && await isDirectoryInput(files[0]);
31299
31678
  let inputs;
31300
31679
  if (folder) {
@@ -31311,13 +31690,11 @@ function registerShareCommand(program2) {
31311
31690
  inputs = await Promise.all(files.map((file) => readShareInput(file, name, maxBytes)));
31312
31691
  }
31313
31692
  assertAggregateInputLimit(inputs, maxBytes);
31314
- const actions = requestedActions(options.action);
31315
31693
  if (options.prefix && target.kind === "bearer") throw new CLIError("INVALID_ARGUMENT", "--prefix requires an addressed target", 2);
31316
31694
  if (target.kind === "bearer" && actions.includes("edit")) throw new CLIError("INVALID_ARGUMENT", `--action edit is not available with --to ${options.to}: a link can only be read; address a recipient to allow edits`, 2);
31317
31695
  if (inputs.length > 1 && target.kind === "bearer") throw new CLIError("UNSUPPORTED_LINK", "multiple files require an addressed target", 2);
31318
31696
  const artifact = artifactRequest(options, actions, inputs.length);
31319
31697
  if (inputs.length > 1 && !options.prefix) throw new CLIError("INVALID_ARGUMENT", "multiple files require --prefix", 2);
31320
- if (options.notify === true && !actions.includes("read")) throw new CLIError("INVALID_ARGUMENT", "--notify requires the read action; nothing was shared", 2);
31321
31698
  if (options.notifyTo !== void 0 && (options.notify !== true || target.kind !== "emailDomain")) {
31322
31699
  throw new CLIError("INVALID_ARGUMENT", "--notify-to requires --to domain:<name> --notify", 2);
31323
31700
  }
@@ -31347,6 +31724,7 @@ function registerShareCommand(program2) {
31347
31724
  origin: options.viewerOrigin,
31348
31725
  ...maxBytes === void 0 ? {} : { maxBytes },
31349
31726
  notify: options.notify === true,
31727
+ ...credentialDescriptor === void 0 ? {} : { credentialDescriptor, issuerDid: options.issuerDid, issuerKid: options.issuerKid },
31350
31728
  // The artifact request rides on the adapter call rather than through `publishTargetShare`, whose code is part of
31351
31729
  // the browser bundles (the web-sdk size ceiling counts every byte it carries).
31352
31730
  targetAdapter: artifact === void 0 || shareServices.targetAdapter === void 0 ? shareServices.targetAdapter : { publish: (request) => shareServices.targetAdapter.publish({ ...request, artifact }) }