@tinycloud/share-sdk 1.2.0-beta.3 → 1.2.0-beta.5

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
@@ -5341,6 +5341,20 @@ var v3TargetSchema = external_exports.object({
5341
5341
  nodeAudience: external_exports.string().min(1),
5342
5342
  spaceId: external_exports.string().refine(isCanonicalPathSegment, { message: "expected a canonical space id" })
5343
5343
  }).strict();
5344
+ var ARTIFACT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{2,62}$/;
5345
+ var artifactPinSchema = external_exports.object({
5346
+ v: external_exports.literal(1),
5347
+ artifactId: external_exports.string().regex(ARTIFACT_ID_PATTERN),
5348
+ version: external_exports.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
5349
+ /** SHA-256 (lowercase hex) of the exact `manifest.json` bytes. The manifest itself stays an owner-side record. */
5350
+ manifestSha256: external_exports.string().regex(/^[0-9a-f]{64}$/),
5351
+ /** An addressed share reads one exact file, so exactly one entry: the stored bytes (ciphertext) it will fetch. */
5352
+ files: external_exports.array(external_exports.object({
5353
+ path: external_exports.string().min(1).max(255),
5354
+ sha256: external_exports.string().regex(/^[0-9a-f]{64}$/),
5355
+ bytes: external_exports.number().int().nonnegative()
5356
+ }).strict()).length(1)
5357
+ }).strict();
5344
5358
  var unsignedShareEnvelopeV3BaseSchema = external_exports.object({
5345
5359
  version: external_exports.literal(3),
5346
5360
  shareId: external_exports.string().min(1),
@@ -5360,7 +5374,13 @@ var unsignedShareEnvelopeV3BaseSchema = external_exports.object({
5360
5374
  expiry: external_exports.string().datetime({ offset: true }),
5361
5375
  display: displaySchema,
5362
5376
  encrypted: external_exports.literal(true),
5363
- metadata: contentMetadataSchema
5377
+ metadata: contentMetadataSchema,
5378
+ /**
5379
+ * TC-1428: an owner-signed statement that this share reads one write-once artifact version. Additive: absent on
5380
+ * every other share, and covered by the envelope signature because the signature spans the whole unsigned object.
5381
+ * The shape is checked here; its binding to the content source is `verifyArtifactShare` in share-sdk.
5382
+ */
5383
+ artifactPin: artifactPinSchema.optional()
5364
5384
  }).strict();
5365
5385
  function validateV3Invariants(value, ctx) {
5366
5386
  const actions = [...value.actions];
@@ -5952,6 +5972,84 @@ async function parseSealedInlineShareUrl(url, options = {}) {
5952
5972
  return { kind: "inline", ciphertextCid: record.cid, ciphertext, key32 };
5953
5973
  }
5954
5974
 
5975
+ // src/artifact-core.ts
5976
+ var ARTIFACT_ROOT = "artifacts";
5977
+ function parseArtifactVersionPath(path) {
5978
+ const match = /^artifacts\/([a-z0-9][a-z0-9_-]{2,62})\/versions\/([1-9][0-9]{0,14})\/([^/]+)$/.exec(path);
5979
+ if (match === null) return void 0;
5980
+ return { artifactId: match[1], version: Number(match[2]), file: match[3] };
5981
+ }
5982
+
5983
+ // src/artifact-pin.ts
5984
+ var ArtifactPinError = class extends Error {
5985
+ code;
5986
+ /** The code is the message: stable, short, and what callers and tests match on. */
5987
+ constructor(code3) {
5988
+ super(code3);
5989
+ this.name = "ArtifactPinError";
5990
+ this.code = code3;
5991
+ }
5992
+ };
5993
+ function artifactBindingView(envelope) {
5994
+ return {
5995
+ ...envelope.artifactPin === void 0 ? {} : { artifactPin: envelope.artifactPin },
5996
+ target: envelope.target,
5997
+ contentSource: envelope.contentSource,
5998
+ resource: envelope.resource,
5999
+ actions: envelope.actions,
6000
+ policyKvActions: envelope.policy.capabilityCeiling.flatMap((capability) => capability.kind === "kv" ? capability.actions : [])
6001
+ };
6002
+ }
6003
+ function sourcePath(view) {
6004
+ const resource = view.contentSource.kvResource;
6005
+ const marker = "/kv/";
6006
+ const at = resource.indexOf(marker);
6007
+ if (at <= 0 || resource.slice(0, at) !== view.target.spaceId) {
6008
+ throw new ArtifactPinError("pin-source-mismatch");
6009
+ }
6010
+ return resource.slice(at + marker.length);
6011
+ }
6012
+ function assertArtifactBinding(view, expected = {}) {
6013
+ const pin = view.artifactPin;
6014
+ const path = sourcePath(view);
6015
+ const inArtifacts = path === ARTIFACT_ROOT || path.startsWith(`${ARTIFACT_ROOT}/`);
6016
+ if (!inArtifacts) {
6017
+ if (pin !== void 0) throw new ArtifactPinError("pin-source-mismatch");
6018
+ return void 0;
6019
+ }
6020
+ if (pin === void 0) throw new ArtifactPinError("missing-pin");
6021
+ const parsed = parseArtifactVersionPath(path);
6022
+ if (parsed === void 0) throw new ArtifactPinError("unsupported-artifact-path");
6023
+ const file = pin.files[0];
6024
+ const source = view.contentSource;
6025
+ if (
6026
+ // The stored ciphertext of one file: bounded like content (100 MiB) plus the envelope's own framing.
6027
+ file.bytes > 128 * 1024 * 1024 || pin.artifactId !== parsed.artifactId || pin.version !== parsed.version || file.path !== parsed.file || view.resource.kind !== "exact" || view.resource.path !== path || source.selector !== "exact" || source.mode !== "immutable" || source.initialCiphertextDigestHex !== file.sha256
6028
+ ) {
6029
+ throw new ArtifactPinError("pin-source-mismatch");
6030
+ }
6031
+ if (view.actions.includes("edit") || view.policyKvActions.includes("tinycloud.kv/put")) {
6032
+ throw new ArtifactPinError("pinned-share-writable");
6033
+ }
6034
+ if (expected.artifactId !== void 0 && expected.artifactId !== pin.artifactId || expected.version !== void 0 && expected.version !== pin.version) {
6035
+ throw new ArtifactPinError("expectation-mismatch");
6036
+ }
6037
+ return pin;
6038
+ }
6039
+ function verifyArtifactShare(envelope, options) {
6040
+ if (envelope.signature.signerDid !== options.expectedOwnerDid) {
6041
+ throw new ArtifactPinError("wrong-signer");
6042
+ }
6043
+ let valid;
6044
+ try {
6045
+ valid = verifyEnvelopeV3SignatureOnly(envelope) && envelope.policy.ownerDid === options.expectedOwnerDid;
6046
+ } catch {
6047
+ valid = false;
6048
+ }
6049
+ if (!valid) throw new ArtifactPinError("bad-signature");
6050
+ return assertArtifactBinding(artifactBindingView(envelope), options.expected);
6051
+ }
6052
+
5955
6053
  // src/receive.ts
5956
6054
  var SHARE_RESULT_VERSION = 1;
5957
6055
  var DEFAULT_MAX_SEALED_BLOB_BYTES = 100 * 1024 * 1024 + 29;
@@ -5988,7 +6086,8 @@ function metadataFor(envelope, origin) {
5988
6086
  ...envelope.display.senderName === void 0 ? {} : { senderName: envelope.display.senderName },
5989
6087
  ...envelope.display.filename === void 0 ? {} : { filename: envelope.display.filename },
5990
6088
  ...envelope.display.mode === void 0 ? {} : { mode: envelope.display.mode }
5991
- }
6089
+ },
6090
+ ...envelope.artifactPin === void 0 ? {} : { artifact: { id: envelope.artifactPin.artifactId, version: envelope.artifactPin.version } }
5992
6091
  };
5993
6092
  }
5994
6093
  async function resolvePolicyShare(link, options) {
@@ -6019,6 +6118,12 @@ async function resolvePolicyShare(link, options) {
6019
6118
  } catch {
6020
6119
  throw new ShareReceiveError("signature-invalid", "share signature is invalid");
6021
6120
  }
6121
+ try {
6122
+ verifyArtifactShare(envelope, { expectedOwnerDid: envelope.policy.ownerDid });
6123
+ } catch (error) {
6124
+ if (!(error instanceof ArtifactPinError)) throw error;
6125
+ throw new ShareReceiveError("envelope-invalid", `share artifact pin is invalid (${error.code})`);
6126
+ }
6022
6127
  options.signal?.throwIfAborted();
6023
6128
  const expiry = Date.parse(envelope.expiry);
6024
6129
  if (!Number.isFinite(expiry)) throw new ShareReceiveError("envelope-invalid", "share expiry is invalid");
@@ -6737,6 +6842,14 @@ function assertMailboxCommitment(commitment, target) {
6737
6842
  async function publishAddressedShare(input) {
6738
6843
  assertSafeInput(input);
6739
6844
  const { target } = prepareAddressedShare(input);
6845
+ assertArtifactBinding({
6846
+ ...input.artifactPin === void 0 ? {} : { artifactPin: input.artifactPin },
6847
+ target: { spaceId: input.spaceId },
6848
+ contentSource: input.contentSource,
6849
+ resource: input.resource,
6850
+ actions: input.actions,
6851
+ policyKvActions: input.policyActions
6852
+ });
6740
6853
  const options = { ...input, filename: canonicalShareFilename(input.filename) };
6741
6854
  if (target.kind !== "recipientDid") assertMailboxCommitment(options.credentialRequirement, target);
6742
6855
  const expiry = rfc3339Seconds(options.expiresAt);
@@ -6803,7 +6916,8 @@ async function publishAddressedShare(input) {
6803
6916
  filename: options.filename,
6804
6917
  ...options.mediaType.startsWith("text/") ? { encoding: "utf-8" } : {},
6805
6918
  ...options.artifact === void 0 ? {} : { artifact: options.artifact }
6806
- }
6919
+ },
6920
+ ...options.artifactPin === void 0 ? {} : { artifactPin: options.artifactPin }
6807
6921
  };
6808
6922
  unsignedShareEnvelopeV3Schema.parse(unsigned);
6809
6923
  const envelopeSignature = await options.authority.sign(sha2563(textEncoder.encode(`${ENVELOPE_V3_DOMAIN}${canonicalize(unsigned)}`)));
@@ -8497,6 +8611,7 @@ var ShareRecipientClient = class {
8497
8611
  const envelope = this.options.envelope;
8498
8612
  const signer = this.nativeSigner ?? this.signer;
8499
8613
  if (envelope.version !== 3 || this.session === void 0 || this.v3Authorization === void 0 || this.v3NodeAudience === void 0 || signer === void 0) throw new Error("v3 policy session signer is required");
8614
+ assertArtifactBinding(artifactBindingView(envelope));
8500
8615
  if (!matchesInitialCiphertextDigest(bytes3, envelope.contentSource.initialCiphertextDigestHex)) throw new Error("v3 owner KV ciphertext digest mismatch");
8501
8616
  const encrypted = parseV3InlineEncryptedEnvelope(bytes3, envelope);
8502
8617
  const receiverPrivateKey = crypto.getRandomValues(new Uint8Array(32));
@@ -8685,6 +8800,7 @@ async function openNativeShare(sharing, link) {
8685
8800
  return sharing.receive(parseNativeShareUrl(link), { autoSubdelegate: false, useSessionKey: false });
8686
8801
  }
8687
8802
  export {
8803
+ ArtifactPinError,
8688
8804
  CredentialInvitationError,
8689
8805
  DEFAULT_MAX_CONTENT_BLOB_BYTES,
8690
8806
  DEFAULT_MAX_SEALED_BLOB_BYTES,
@@ -8707,6 +8823,8 @@ export {
8707
8823
  ShareReceiveError,
8708
8824
  ShareRecipientClient,
8709
8825
  addressedCredentialRequirement,
8826
+ artifactBindingView,
8827
+ assertArtifactBinding,
8710
8828
  authorizationMethodForTarget,
8711
8829
  authorizeShare,
8712
8830
  canonicalOwnerSharePolicy,
@@ -8749,6 +8867,7 @@ export {
8749
8867
  toBase64Url,
8750
8868
  toShareErrorInfo,
8751
8869
  validateOwnerSharePolicyRegistration,
8752
- validateOwnerSharePolicyRegistrationBytes
8870
+ validateOwnerSharePolicyRegistrationBytes,
8871
+ verifyArtifactShare
8753
8872
  };
8754
8873
  //# sourceMappingURL=index.js.map