@odla-ai/brand 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -534,6 +534,15 @@ interface BrandDiscussionReferenceTarget {
534
534
  bookId: string;
535
535
  resourceId?: string;
536
536
  }
537
+ /** Bounded color projection shared with Discussion people and agents. */
538
+ interface BrandDiscussionSwatch {
539
+ /** Semantic role used by the Brand token compiler. */
540
+ role: string;
541
+ /** Normalized opaque sRGB color (`#rrggbb`). */
542
+ hex: string;
543
+ /** Optional product-authored color name. */
544
+ name?: string;
545
+ }
537
546
  /** Product-owned projection returned to Discussion typeahead and exact lookup. */
538
547
  interface BrandDiscussionReference {
539
548
  /** Canonical Brand kind used in stored Discussion ref markup. */
@@ -552,6 +561,8 @@ interface BrandDiscussionReference {
552
561
  destination: string;
553
562
  /** Same-origin native link; navigation is not mutation authority. */
554
563
  href: string;
564
+ /** Present only for a validated Brand palette, capped by Brand's swatch limit. */
565
+ swatches?: BrandDiscussionSwatch[];
555
566
  }
556
567
  /** Durable ref id: a book id, or `bookId/resourceId` for a child resource. */
557
568
  declare function brandDiscussionReferenceId(target: BrandDiscussionReferenceTarget): string;
@@ -1274,6 +1285,11 @@ interface BrandRouteOpts {
1274
1285
  basePath?: string;
1275
1286
  /** Native UI mount used by returned Discussion deep links. Default "/". */
1276
1287
  discussionBasePath?: string;
1288
+ /**
1289
+ * Product-owned fetch used only for a short-lived URL minted from a linked
1290
+ * private Brand asset. Callers never provide this URL or receive it back.
1291
+ */
1292
+ fetchPrivateAsset?: typeof fetch;
1277
1293
  /** Upload size cap in bytes (checked against `File.size`). Default 8 MiB. */
1278
1294
  maxUploadBytes?: number;
1279
1295
  /** Serve `GET /books/:id/tokens.css` and `tokens.json` without authorize —
@@ -1296,6 +1312,7 @@ interface BrandRouteCtx {
1296
1312
  newId: () => string;
1297
1313
  maxUploadBytes: number;
1298
1314
  discussionBasePath: string;
1315
+ fetchPrivateAsset: typeof fetch;
1299
1316
  }
1300
1317
 
1301
1318
  /** Freeze the exact OPEN proposal fields a reviewer sees. */
@@ -1513,4 +1530,4 @@ interface CreateBrandIntegrationOptions {
1513
1530
  */
1514
1531
  declare function createBrandIntegration(options?: CreateBrandIntegrationOptions): BrandIntegrationDescriptor;
1515
1532
 
1516
- export { ASSET_CONTENT_TYPES, type AcceptProposalInput, AssetAnalysis, AssetKind, type AttrType, type AudienceChildren, BRAND_AGENT_PROFILE, BRAND_DISCUSSION_REFERENCE_KINDS, BRAND_INSTRUCTIONS, BRAND_RULES, BRAND_SCHEMA, BookStatus, type BrandActor, type BrandAgentBridge, type BrandAgentProfile, type BrandApprovalReceipt, type BrandAsset, type BrandAttrs, type BrandBook, type BrandBotTriggerOpts, type BrandCapabilityAuthority, BrandConflictError, type BrandDb, type BrandDecisionBinding, type BrandDeps, type BrandDiscussionReference, type BrandDiscussionReferenceKind, type BrandDiscussionReferenceTarget, type BrandDispatchBody, type BrandDispatchDeps, type BrandEntityRef, type BrandFetchedBytes, type BrandFileRecord, BrandForbiddenError, BrandGoneError, type BrandHumanAuthorityConsumption, BrandInputError, type BrandIntegrationDescriptor, type BrandIntegrationProbe, type BrandLookup, BrandNotFoundError, type BrandOp, type BrandPalette, type BrandPrincipalProjection, type BrandProposal, type BrandProposalProvenance, type BrandProposalReviewSnapshot, type BrandResult, BrandReviewStateChangedError, type BrandRouteCtx, type BrandRouteOpts, type BrandRow, type BrandRule, type BrandRules, type BrandScalar, type BrandSection, type BrandSkillOpts, type BrandSkillSelf, type BrandSourceAssetSnapshot, type BrandStorage, BrandTokensSnapshot, type BrandToolCtx, type BrandTransactGuard, type BrandTransactOptions, type BrandTrigger, type BrandUploadBody, type BrandVisionCapabilities, type BrandVisionSpec, CHART_DELTA_MIN, CHAT_MESSAGE_NS, CSS_NAMED_COLORS, type ContrastKind, type CreateAssetInput, type CreateBookInput, type CreateBrandIntegrationOptions, type CreateBrandPersonaOpts, DEFAULT_BRAND_SYSTEM, type DeriveChartOptions, type DerivePaletteOptions, type Hsl, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type LightnessDirection, MAX_SWATCHES, MAX_VIEW_BYTES, type NamedColor, type NearestNamedColor, type Oklab, type Oklch, PICK_TEXT_DEFAULT_CANDIDATES, PaletteSource, PaletteStatus, ProposalKind, ProposalStatus, type ProposePaletteInput, type ProposeSectionInput, RAMP_L_MAX, RAMP_L_MIN, type RejectProposalInput, type ResolvedBrandDeps, type Rgb, SectionKind, SectionStatus, type SerializedAttr, type SerializedEntity, type SerializedLink, type SerializedLinkEnd, type SerializedSchema, Swatch, type UpsertSectionInput, acceptProposalOps, adjustLightnessUntil, analogous, assertAnalysis, assertAssetContentType, assertHex, assertSectionContent, assertSwatches, assetTools, attachedBrandAssetIds, audienceFanoutOps, base64FromBytes, beginAssetDeleteOps, bookForChannel, bookTools, brandAuthorityRef, brandBotTrigger, brandDiscussionReferenceHref, brandDiscussionReferenceId, brandInputFor, brandIntegration, brandJsonDigest, brandRules, brandSkill, canonicalBrandJson, capString, capStringArray, clamp01, clampToGamut, complementary, contrastRatio, createAssetOps, createBookOps, createBrandIntegration, createBrandPersona, createBrandRoutes, deltaEOK, deltaEOKLab, deriveChartColors, derivePalette, dispatchBrandTurn, finishAssetDeleteOps, formatBrandDiscussionReference, hexToOklch, hslToRgb, inSrgbGamut, isBoundedBrandJson, isBrandHumanAuthorityConsumption, linearToSrgb, meetsAA, meetsAAA, monochrome, nearestNamedColor, normalizeHex, oklabToOklch, oklabToRgb, oklchToHex, oklchToOklab, paletteTools, parseBrandDiscussionReference, parseBrandDispatch, parseHex, pickTextOn, proposalReviewSnapshot, proposePaletteOps, proposeSectionOps, readTools, recordAnalysisOps, rejectProposalOps, relativeLuminance, resolveDeps, rgbToHsl, rgbToOklab, rotateHue, safeFileName, sectionKey, splitComplementary, srgbToLinear, supportsBrandVision, tetradic, tintShadeRamp, toHex, triadic, updateBookOps, upsertSectionOps, verifyBrandApprovalReceipt };
1533
+ export { ASSET_CONTENT_TYPES, type AcceptProposalInput, AssetAnalysis, AssetKind, type AttrType, type AudienceChildren, BRAND_AGENT_PROFILE, BRAND_DISCUSSION_REFERENCE_KINDS, BRAND_INSTRUCTIONS, BRAND_RULES, BRAND_SCHEMA, BookStatus, type BrandActor, type BrandAgentBridge, type BrandAgentProfile, type BrandApprovalReceipt, type BrandAsset, type BrandAttrs, type BrandBook, type BrandBotTriggerOpts, type BrandCapabilityAuthority, BrandConflictError, type BrandDb, type BrandDecisionBinding, type BrandDeps, type BrandDiscussionReference, type BrandDiscussionReferenceKind, type BrandDiscussionReferenceTarget, type BrandDiscussionSwatch, type BrandDispatchBody, type BrandDispatchDeps, type BrandEntityRef, type BrandFetchedBytes, type BrandFileRecord, BrandForbiddenError, BrandGoneError, type BrandHumanAuthorityConsumption, BrandInputError, type BrandIntegrationDescriptor, type BrandIntegrationProbe, type BrandLookup, BrandNotFoundError, type BrandOp, type BrandPalette, type BrandPrincipalProjection, type BrandProposal, type BrandProposalProvenance, type BrandProposalReviewSnapshot, type BrandResult, BrandReviewStateChangedError, type BrandRouteCtx, type BrandRouteOpts, type BrandRow, type BrandRule, type BrandRules, type BrandScalar, type BrandSection, type BrandSkillOpts, type BrandSkillSelf, type BrandSourceAssetSnapshot, type BrandStorage, BrandTokensSnapshot, type BrandToolCtx, type BrandTransactGuard, type BrandTransactOptions, type BrandTrigger, type BrandUploadBody, type BrandVisionCapabilities, type BrandVisionSpec, CHART_DELTA_MIN, CHAT_MESSAGE_NS, CSS_NAMED_COLORS, type ContrastKind, type CreateAssetInput, type CreateBookInput, type CreateBrandIntegrationOptions, type CreateBrandPersonaOpts, DEFAULT_BRAND_SYSTEM, type DeriveChartOptions, type DerivePaletteOptions, type Hsl, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type LightnessDirection, MAX_SWATCHES, MAX_VIEW_BYTES, type NamedColor, type NearestNamedColor, type Oklab, type Oklch, PICK_TEXT_DEFAULT_CANDIDATES, PaletteSource, PaletteStatus, ProposalKind, ProposalStatus, type ProposePaletteInput, type ProposeSectionInput, RAMP_L_MAX, RAMP_L_MIN, type RejectProposalInput, type ResolvedBrandDeps, type Rgb, SectionKind, SectionStatus, type SerializedAttr, type SerializedEntity, type SerializedLink, type SerializedLinkEnd, type SerializedSchema, Swatch, type UpsertSectionInput, acceptProposalOps, adjustLightnessUntil, analogous, assertAnalysis, assertAssetContentType, assertHex, assertSectionContent, assertSwatches, assetTools, attachedBrandAssetIds, audienceFanoutOps, base64FromBytes, beginAssetDeleteOps, bookForChannel, bookTools, brandAuthorityRef, brandBotTrigger, brandDiscussionReferenceHref, brandDiscussionReferenceId, brandInputFor, brandIntegration, brandJsonDigest, brandRules, brandSkill, canonicalBrandJson, capString, capStringArray, clamp01, clampToGamut, complementary, contrastRatio, createAssetOps, createBookOps, createBrandIntegration, createBrandPersona, createBrandRoutes, deltaEOK, deltaEOKLab, deriveChartColors, derivePalette, dispatchBrandTurn, finishAssetDeleteOps, formatBrandDiscussionReference, hexToOklch, hslToRgb, inSrgbGamut, isBoundedBrandJson, isBrandHumanAuthorityConsumption, linearToSrgb, meetsAA, meetsAAA, monochrome, nearestNamedColor, normalizeHex, oklabToOklch, oklabToRgb, oklchToHex, oklchToOklab, paletteTools, parseBrandDiscussionReference, parseBrandDispatch, parseHex, pickTextOn, proposalReviewSnapshot, proposePaletteOps, proposeSectionOps, readTools, recordAnalysisOps, rejectProposalOps, relativeLuminance, resolveDeps, rgbToHsl, rgbToOklab, rotateHue, safeFileName, sectionKey, splitComplementary, srgbToLinear, supportsBrandVision, tetradic, tintShadeRamp, toHex, triadic, updateBookOps, upsertSectionOps, verifyBrandApprovalReceipt };
package/dist/index.d.ts CHANGED
@@ -534,6 +534,15 @@ interface BrandDiscussionReferenceTarget {
534
534
  bookId: string;
535
535
  resourceId?: string;
536
536
  }
537
+ /** Bounded color projection shared with Discussion people and agents. */
538
+ interface BrandDiscussionSwatch {
539
+ /** Semantic role used by the Brand token compiler. */
540
+ role: string;
541
+ /** Normalized opaque sRGB color (`#rrggbb`). */
542
+ hex: string;
543
+ /** Optional product-authored color name. */
544
+ name?: string;
545
+ }
537
546
  /** Product-owned projection returned to Discussion typeahead and exact lookup. */
538
547
  interface BrandDiscussionReference {
539
548
  /** Canonical Brand kind used in stored Discussion ref markup. */
@@ -552,6 +561,8 @@ interface BrandDiscussionReference {
552
561
  destination: string;
553
562
  /** Same-origin native link; navigation is not mutation authority. */
554
563
  href: string;
564
+ /** Present only for a validated Brand palette, capped by Brand's swatch limit. */
565
+ swatches?: BrandDiscussionSwatch[];
555
566
  }
556
567
  /** Durable ref id: a book id, or `bookId/resourceId` for a child resource. */
557
568
  declare function brandDiscussionReferenceId(target: BrandDiscussionReferenceTarget): string;
@@ -1274,6 +1285,11 @@ interface BrandRouteOpts {
1274
1285
  basePath?: string;
1275
1286
  /** Native UI mount used by returned Discussion deep links. Default "/". */
1276
1287
  discussionBasePath?: string;
1288
+ /**
1289
+ * Product-owned fetch used only for a short-lived URL minted from a linked
1290
+ * private Brand asset. Callers never provide this URL or receive it back.
1291
+ */
1292
+ fetchPrivateAsset?: typeof fetch;
1277
1293
  /** Upload size cap in bytes (checked against `File.size`). Default 8 MiB. */
1278
1294
  maxUploadBytes?: number;
1279
1295
  /** Serve `GET /books/:id/tokens.css` and `tokens.json` without authorize —
@@ -1296,6 +1312,7 @@ interface BrandRouteCtx {
1296
1312
  newId: () => string;
1297
1313
  maxUploadBytes: number;
1298
1314
  discussionBasePath: string;
1315
+ fetchPrivateAsset: typeof fetch;
1299
1316
  }
1300
1317
 
1301
1318
  /** Freeze the exact OPEN proposal fields a reviewer sees. */
@@ -1513,4 +1530,4 @@ interface CreateBrandIntegrationOptions {
1513
1530
  */
1514
1531
  declare function createBrandIntegration(options?: CreateBrandIntegrationOptions): BrandIntegrationDescriptor;
1515
1532
 
1516
- export { ASSET_CONTENT_TYPES, type AcceptProposalInput, AssetAnalysis, AssetKind, type AttrType, type AudienceChildren, BRAND_AGENT_PROFILE, BRAND_DISCUSSION_REFERENCE_KINDS, BRAND_INSTRUCTIONS, BRAND_RULES, BRAND_SCHEMA, BookStatus, type BrandActor, type BrandAgentBridge, type BrandAgentProfile, type BrandApprovalReceipt, type BrandAsset, type BrandAttrs, type BrandBook, type BrandBotTriggerOpts, type BrandCapabilityAuthority, BrandConflictError, type BrandDb, type BrandDecisionBinding, type BrandDeps, type BrandDiscussionReference, type BrandDiscussionReferenceKind, type BrandDiscussionReferenceTarget, type BrandDispatchBody, type BrandDispatchDeps, type BrandEntityRef, type BrandFetchedBytes, type BrandFileRecord, BrandForbiddenError, BrandGoneError, type BrandHumanAuthorityConsumption, BrandInputError, type BrandIntegrationDescriptor, type BrandIntegrationProbe, type BrandLookup, BrandNotFoundError, type BrandOp, type BrandPalette, type BrandPrincipalProjection, type BrandProposal, type BrandProposalProvenance, type BrandProposalReviewSnapshot, type BrandResult, BrandReviewStateChangedError, type BrandRouteCtx, type BrandRouteOpts, type BrandRow, type BrandRule, type BrandRules, type BrandScalar, type BrandSection, type BrandSkillOpts, type BrandSkillSelf, type BrandSourceAssetSnapshot, type BrandStorage, BrandTokensSnapshot, type BrandToolCtx, type BrandTransactGuard, type BrandTransactOptions, type BrandTrigger, type BrandUploadBody, type BrandVisionCapabilities, type BrandVisionSpec, CHART_DELTA_MIN, CHAT_MESSAGE_NS, CSS_NAMED_COLORS, type ContrastKind, type CreateAssetInput, type CreateBookInput, type CreateBrandIntegrationOptions, type CreateBrandPersonaOpts, DEFAULT_BRAND_SYSTEM, type DeriveChartOptions, type DerivePaletteOptions, type Hsl, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type LightnessDirection, MAX_SWATCHES, MAX_VIEW_BYTES, type NamedColor, type NearestNamedColor, type Oklab, type Oklch, PICK_TEXT_DEFAULT_CANDIDATES, PaletteSource, PaletteStatus, ProposalKind, ProposalStatus, type ProposePaletteInput, type ProposeSectionInput, RAMP_L_MAX, RAMP_L_MIN, type RejectProposalInput, type ResolvedBrandDeps, type Rgb, SectionKind, SectionStatus, type SerializedAttr, type SerializedEntity, type SerializedLink, type SerializedLinkEnd, type SerializedSchema, Swatch, type UpsertSectionInput, acceptProposalOps, adjustLightnessUntil, analogous, assertAnalysis, assertAssetContentType, assertHex, assertSectionContent, assertSwatches, assetTools, attachedBrandAssetIds, audienceFanoutOps, base64FromBytes, beginAssetDeleteOps, bookForChannel, bookTools, brandAuthorityRef, brandBotTrigger, brandDiscussionReferenceHref, brandDiscussionReferenceId, brandInputFor, brandIntegration, brandJsonDigest, brandRules, brandSkill, canonicalBrandJson, capString, capStringArray, clamp01, clampToGamut, complementary, contrastRatio, createAssetOps, createBookOps, createBrandIntegration, createBrandPersona, createBrandRoutes, deltaEOK, deltaEOKLab, deriveChartColors, derivePalette, dispatchBrandTurn, finishAssetDeleteOps, formatBrandDiscussionReference, hexToOklch, hslToRgb, inSrgbGamut, isBoundedBrandJson, isBrandHumanAuthorityConsumption, linearToSrgb, meetsAA, meetsAAA, monochrome, nearestNamedColor, normalizeHex, oklabToOklch, oklabToRgb, oklchToHex, oklchToOklab, paletteTools, parseBrandDiscussionReference, parseBrandDispatch, parseHex, pickTextOn, proposalReviewSnapshot, proposePaletteOps, proposeSectionOps, readTools, recordAnalysisOps, rejectProposalOps, relativeLuminance, resolveDeps, rgbToHsl, rgbToOklab, rotateHue, safeFileName, sectionKey, splitComplementary, srgbToLinear, supportsBrandVision, tetradic, tintShadeRamp, toHex, triadic, updateBookOps, upsertSectionOps, verifyBrandApprovalReceipt };
1533
+ export { ASSET_CONTENT_TYPES, type AcceptProposalInput, AssetAnalysis, AssetKind, type AttrType, type AudienceChildren, BRAND_AGENT_PROFILE, BRAND_DISCUSSION_REFERENCE_KINDS, BRAND_INSTRUCTIONS, BRAND_RULES, BRAND_SCHEMA, BookStatus, type BrandActor, type BrandAgentBridge, type BrandAgentProfile, type BrandApprovalReceipt, type BrandAsset, type BrandAttrs, type BrandBook, type BrandBotTriggerOpts, type BrandCapabilityAuthority, BrandConflictError, type BrandDb, type BrandDecisionBinding, type BrandDeps, type BrandDiscussionReference, type BrandDiscussionReferenceKind, type BrandDiscussionReferenceTarget, type BrandDiscussionSwatch, type BrandDispatchBody, type BrandDispatchDeps, type BrandEntityRef, type BrandFetchedBytes, type BrandFileRecord, BrandForbiddenError, BrandGoneError, type BrandHumanAuthorityConsumption, BrandInputError, type BrandIntegrationDescriptor, type BrandIntegrationProbe, type BrandLookup, BrandNotFoundError, type BrandOp, type BrandPalette, type BrandPrincipalProjection, type BrandProposal, type BrandProposalProvenance, type BrandProposalReviewSnapshot, type BrandResult, BrandReviewStateChangedError, type BrandRouteCtx, type BrandRouteOpts, type BrandRow, type BrandRule, type BrandRules, type BrandScalar, type BrandSection, type BrandSkillOpts, type BrandSkillSelf, type BrandSourceAssetSnapshot, type BrandStorage, BrandTokensSnapshot, type BrandToolCtx, type BrandTransactGuard, type BrandTransactOptions, type BrandTrigger, type BrandUploadBody, type BrandVisionCapabilities, type BrandVisionSpec, CHART_DELTA_MIN, CHAT_MESSAGE_NS, CSS_NAMED_COLORS, type ContrastKind, type CreateAssetInput, type CreateBookInput, type CreateBrandIntegrationOptions, type CreateBrandPersonaOpts, DEFAULT_BRAND_SYSTEM, type DeriveChartOptions, type DerivePaletteOptions, type Hsl, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type LightnessDirection, MAX_SWATCHES, MAX_VIEW_BYTES, type NamedColor, type NearestNamedColor, type Oklab, type Oklch, PICK_TEXT_DEFAULT_CANDIDATES, PaletteSource, PaletteStatus, ProposalKind, ProposalStatus, type ProposePaletteInput, type ProposeSectionInput, RAMP_L_MAX, RAMP_L_MIN, type RejectProposalInput, type ResolvedBrandDeps, type Rgb, SectionKind, SectionStatus, type SerializedAttr, type SerializedEntity, type SerializedLink, type SerializedLinkEnd, type SerializedSchema, Swatch, type UpsertSectionInput, acceptProposalOps, adjustLightnessUntil, analogous, assertAnalysis, assertAssetContentType, assertHex, assertSectionContent, assertSwatches, assetTools, attachedBrandAssetIds, audienceFanoutOps, base64FromBytes, beginAssetDeleteOps, bookForChannel, bookTools, brandAuthorityRef, brandBotTrigger, brandDiscussionReferenceHref, brandDiscussionReferenceId, brandInputFor, brandIntegration, brandJsonDigest, brandRules, brandSkill, canonicalBrandJson, capString, capStringArray, clamp01, clampToGamut, complementary, contrastRatio, createAssetOps, createBookOps, createBrandIntegration, createBrandPersona, createBrandRoutes, deltaEOK, deltaEOKLab, deriveChartColors, derivePalette, dispatchBrandTurn, finishAssetDeleteOps, formatBrandDiscussionReference, hexToOklch, hslToRgb, inSrgbGamut, isBoundedBrandJson, isBrandHumanAuthorityConsumption, linearToSrgb, meetsAA, meetsAAA, monochrome, nearestNamedColor, normalizeHex, oklabToOklch, oklabToRgb, oklchToHex, oklchToOklab, paletteTools, parseBrandDiscussionReference, parseBrandDispatch, parseHex, pickTextOn, proposalReviewSnapshot, proposePaletteOps, proposeSectionOps, readTools, recordAnalysisOps, rejectProposalOps, relativeLuminance, resolveDeps, rgbToHsl, rgbToOklab, rotateHue, safeFileName, sectionKey, splitComplementary, srgbToLinear, supportsBrandVision, tetradic, tintShadeRamp, toHex, triadic, updateBookOps, upsertSectionOps, verifyBrandApprovalReceipt };
package/dist/index.js CHANGED
@@ -2372,13 +2372,13 @@ async function proposalDecisionOps(input) {
2372
2372
 
2373
2373
  // src/routes/proposal-resolution-receipts.ts
2374
2374
  async function proposalResolutionMutationKey(bookId, proposalId, mutationId) {
2375
- const digest = await brandJsonDigest({
2375
+ const digest2 = await brandJsonDigest({
2376
2376
  version: 3,
2377
2377
  bookId,
2378
2378
  proposalId,
2379
2379
  mutationId
2380
2380
  });
2381
- return `brand:proposal-resolution:v3:${digest.slice("sha256:".length)}`;
2381
+ return `brand:proposal-resolution:v3:${digest2.slice("sha256:".length)}`;
2382
2382
  }
2383
2383
  async function proposalReceiptByMutation(ctx, mutationKey) {
2384
2384
  const result = await ctx.db.query({
@@ -2928,6 +2928,130 @@ async function handleTokens(db, req, bookId, which, actor) {
2928
2928
  });
2929
2929
  }
2930
2930
 
2931
+ // src/routes/discussion-asset.ts
2932
+ var MAX_DISCUSSION_ASSET_BYTES = 4718592;
2933
+ var SIGN_TTL_SECONDS = 30;
2934
+ var TYPES = /* @__PURE__ */ new Set([
2935
+ "image/png",
2936
+ "image/jpeg",
2937
+ "image/gif",
2938
+ "image/webp",
2939
+ "application/pdf"
2940
+ ]);
2941
+ var bare = (value) => (value?.split(";", 1)[0] ?? "").trim().toLowerCase();
2942
+ var starts = (bytes, expected) => expected.every((value, index) => bytes[index] === value);
2943
+ function magicMatches(contentType, bytes) {
2944
+ if (contentType === "image/png") {
2945
+ return starts(bytes, [137, 80, 78, 71, 13, 10, 26, 10]);
2946
+ }
2947
+ if (contentType === "image/jpeg") {
2948
+ return starts(bytes, [255, 216, 255]);
2949
+ }
2950
+ const prefix = new TextDecoder().decode(bytes.subarray(0, 16));
2951
+ if (contentType === "image/gif") {
2952
+ return prefix.startsWith("GIF87a") || prefix.startsWith("GIF89a");
2953
+ }
2954
+ if (contentType === "image/webp") {
2955
+ return prefix.startsWith("RIFF") && prefix.slice(8, 12) === "WEBP";
2956
+ }
2957
+ return contentType === "application/pdf" && prefix.startsWith("%PDF-");
2958
+ }
2959
+ async function boundedBytes(response, max) {
2960
+ const length = Number(response.headers.get("content-length"));
2961
+ if (Number.isFinite(length) && length > max) return null;
2962
+ if (!response.body) return new Uint8Array();
2963
+ const reader = response.body.getReader();
2964
+ const chunks = [];
2965
+ let total = 0;
2966
+ while (true) {
2967
+ const next = await reader.read();
2968
+ if (next.done) break;
2969
+ total += next.value.byteLength;
2970
+ if (total > max) {
2971
+ await reader.cancel();
2972
+ return null;
2973
+ }
2974
+ chunks.push(next.value);
2975
+ }
2976
+ const out = new Uint8Array(total);
2977
+ let offset = 0;
2978
+ for (const chunk of chunks) {
2979
+ out.set(chunk, offset);
2980
+ offset += chunk.byteLength;
2981
+ }
2982
+ return out;
2983
+ }
2984
+ async function digest(bytes) {
2985
+ const value = await crypto.subtle.digest("SHA-256", bytes.slice().buffer);
2986
+ return `sha256:${[...new Uint8Array(value)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2987
+ }
2988
+ function exactAsset(beforeBook, before, afterBook, after) {
2989
+ return afterBook.id === beforeBook.id && after.id === before.id && after.bookId === before.bookId && after.status === "live" && after.path === before.path && after.storageObjectId === before.storageObjectId && after.contentDigest === before.contentDigest && after.contentType === before.contentType && after.size === before.size;
2990
+ }
2991
+ function safeSignedUrl(value) {
2992
+ try {
2993
+ const url = new URL(value);
2994
+ return value.length <= 4096 && url.protocol === "https:" && !url.username && !url.password ? url : null;
2995
+ } catch {
2996
+ return null;
2997
+ }
2998
+ }
2999
+ async function handleBrandDiscussionAsset(ctx, req, target) {
3000
+ const beforeBook = await loadMemberBook(ctx.db, target.bookId, ctx.actor.id);
3001
+ const before = await linkedAsset(ctx, beforeBook, target.resourceId);
3002
+ const contentType = bare(before.contentType);
3003
+ if (!TYPES.has(contentType)) {
3004
+ return json({ error: "asset content type is not viewable" }, 415);
3005
+ }
3006
+ if (before.size < 1 || before.size > MAX_DISCUSSION_ASSET_BYTES) {
3007
+ return json({ error: "asset exceeds the discussion media limit" }, 413);
3008
+ }
3009
+ const signed = safeSignedUrl(
3010
+ await ctx.db.storage.sign(before.path, SIGN_TTL_SECONDS)
3011
+ );
3012
+ if (!signed) throw new BrandInputError("private asset signing failed");
3013
+ const response = await ctx.fetchPrivateAsset(signed, {
3014
+ headers: { accept: contentType },
3015
+ redirect: "manual",
3016
+ signal: req.signal
3017
+ });
3018
+ if (!response.ok || response.type === "opaqueredirect") {
3019
+ throw new BrandNotFoundError(`asset ${before.id}`);
3020
+ }
3021
+ const served = bare(response.headers.get("content-type"));
3022
+ if (served === "image/svg+xml" || served && served !== "application/octet-stream" && served !== contentType) {
3023
+ return json({ error: "asset content type is inconsistent" }, 415);
3024
+ }
3025
+ const bytes = await boundedBytes(response, MAX_DISCUSSION_ASSET_BYTES);
3026
+ if (!bytes || bytes.byteLength !== before.size || !magicMatches(contentType, bytes) || await digest(bytes) !== before.contentDigest) {
3027
+ return json({ error: "asset bytes failed validation" }, 422);
3028
+ }
3029
+ const afterBook = await loadMemberBook(ctx.db, target.bookId, ctx.actor.id);
3030
+ const after = await linkedAsset(ctx, afterBook, target.resourceId);
3031
+ if (!exactAsset(beforeBook, before, afterBook, after)) {
3032
+ return json({ error: "asset changed during inspection" }, 409);
3033
+ }
3034
+ return json({
3035
+ asset: {
3036
+ reference: { kind: "brand:asset", id: `${before.bookId}/${before.id}` },
3037
+ contentType,
3038
+ byteLength: bytes.byteLength,
3039
+ data: base64(bytes),
3040
+ taint: "untrusted_project_material"
3041
+ }
3042
+ }, 200, {
3043
+ "cache-control": "private, no-store",
3044
+ "x-content-type-options": "nosniff"
3045
+ });
3046
+ }
3047
+ function base64(bytes) {
3048
+ let binary = "";
3049
+ for (let index = 0; index < bytes.length; index += 8192) {
3050
+ binary += String.fromCharCode(...bytes.subarray(index, index + 8192));
3051
+ }
3052
+ return btoa(binary);
3053
+ }
3054
+
2931
3055
  // src/routes/discussion-references.ts
2932
3056
  var capLimit = (raw) => {
2933
3057
  const parsed = Number(raw ?? 20);
@@ -2959,6 +3083,7 @@ var projection = (req, ctx, book, target, row) => {
2959
3083
  let summary = `${book.name} brand book`;
2960
3084
  let status = book.status;
2961
3085
  let destination = "Brand Studio \xB7 Brand book";
3086
+ let swatches;
2962
3087
  if (target.kind === "brand:asset") {
2963
3088
  const asset = row;
2964
3089
  label = asset.title?.trim() || `${asset.kind} asset`;
@@ -2968,6 +3093,11 @@ var projection = (req, ctx, book, target, row) => {
2968
3093
  destination = "Brand Studio \xB7 Conversation";
2969
3094
  } else if (target.kind === "brand:palette") {
2970
3095
  const palette = row;
3096
+ swatches = assertSwatches(palette.swatches).map((swatch) => ({
3097
+ role: swatch.role,
3098
+ hex: swatch.hex,
3099
+ ...swatch.name ? { name: swatch.name } : {}
3100
+ }));
2971
3101
  label = palette.name;
2972
3102
  hint = `${book.name} \xB7 ${palette.status} palette`;
2973
3103
  summary = `${palette.swatches.length}-color palette in ${book.name}`;
@@ -3000,7 +3130,8 @@ var projection = (req, ctx, book, target, row) => {
3000
3130
  new URL(req.url),
3001
3131
  target,
3002
3132
  ctx.discussionBasePath
3003
- )
3133
+ ),
3134
+ ...swatches ? { swatches } : {}
3004
3135
  };
3005
3136
  };
3006
3137
  async function exact(ctx, req, target) {
@@ -3054,11 +3185,23 @@ async function search(ctx, req, url) {
3054
3185
  }
3055
3186
  async function handleBrandDiscussionReferences(ctx, req, url) {
3056
3187
  if (req.method !== "GET") return methodNotAllowed();
3188
+ const inspect = url.searchParams.get("inspect");
3057
3189
  const kind = url.searchParams.get("kind");
3058
3190
  const id = url.searchParams.get("id");
3059
3191
  if (kind && !id || !kind && id) return json({ error: "kind and id must be paired" }, 400);
3060
3192
  const target = targetFromQuery(url);
3061
3193
  if ((kind || id) && !target) return json({ items: [] });
3194
+ if (inspect === "asset-content") {
3195
+ const allowed = /* @__PURE__ */ new Set(["inspect", "kind", "id"]);
3196
+ if ([...url.searchParams.keys()].some((key) => !allowed.has(key)) || target?.kind !== "brand:asset" || !target.resourceId) return json({ error: "exact brand asset kind and id required" }, 400);
3197
+ return handleBrandDiscussionAsset(ctx, req, {
3198
+ bookId: target.bookId,
3199
+ resourceId: target.resourceId
3200
+ });
3201
+ }
3202
+ if (inspect !== null && inspect !== "1") {
3203
+ return json({ error: "invalid inspection mode" }, 400);
3204
+ }
3062
3205
  return json({
3063
3206
  items: target ? await exact(ctx, req, target) : await search(ctx, req, url)
3064
3207
  });
@@ -3099,7 +3242,8 @@ function createBrandRoutes(options) {
3099
3242
  now: options.now ?? Date.now,
3100
3243
  newId: options.newId ?? (() => crypto.randomUUID()),
3101
3244
  maxUploadBytes: options.maxUploadBytes ?? 8 * 1024 * 1024,
3102
- discussionBasePath: options.discussionBasePath ?? "/"
3245
+ discussionBasePath: options.discussionBasePath ?? "/",
3246
+ fetchPrivateAsset: options.fetchPrivateAsset ?? fetch
3103
3247
  };
3104
3248
  return async (req) => {
3105
3249
  const url = new URL(req.url);