@runbound/config-browser 0.0.0-stage → 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ import type { BrowserExperimentDurableQueue, BrowserExperimentIdentityState, BrowserExperimentIdentityStore, BrowserExperimentQueueLimits, BrowserExperimentQueueRecord, BrowserExperimentQueueSnapshot } from "./config-experimentation.js";
2
+ type StoredRecord = Readonly<{
3
+ outcome: "acknowledged" | "lost" | "pending";
4
+ record: BrowserExperimentQueueRecord;
5
+ }>;
6
+ export type BrowserExperimentDurabilityState = Readonly<{
7
+ formatVersion: "runbound.browser.experiment-durability-state.v1";
8
+ identity?: BrowserExperimentIdentityState;
9
+ lostByScope: Readonly<Record<string, number>>;
10
+ records: readonly StoredRecord[];
11
+ sequences: Readonly<Record<string, number>>;
12
+ }>;
13
+ export interface BrowserExperimentTransactionalDurability {
14
+ close?(): void;
15
+ transact<T>(mutate: (state: BrowserExperimentDurabilityState) => Readonly<{
16
+ result: T;
17
+ state: BrowserExperimentDurabilityState;
18
+ }>): Promise<T>;
19
+ }
20
+ export type BrowserExperimentDurabilityDiagnostics = Readonly<{
21
+ authorizationEligible: false;
22
+ causalEvidenceOnly: true;
23
+ lostRecords: number;
24
+ pendingRecords: number;
25
+ platform: "web-indexeddb";
26
+ rawIdentifiersIncluded: false;
27
+ supported: true;
28
+ }>;
29
+ export interface BrowserExperimentDurabilityAdapter {
30
+ readonly capability: Readonly<{
31
+ implementation: "indexeddb" | "mobile-host-provided";
32
+ nativeImplementationClaim: false;
33
+ platform: "mobile" | "web";
34
+ transactional: true;
35
+ }>;
36
+ readonly identityStore: BrowserExperimentIdentityStore;
37
+ readonly queue: BrowserExperimentDurableQueue;
38
+ close(): void;
39
+ diagnostics(sessionId: string, runId: string): Promise<BrowserExperimentDurabilityDiagnostics>;
40
+ }
41
+ /**
42
+ * Contract for a future mobile host. This package supplies no native storage
43
+ * implementation and makes no claim about a host until one injects both
44
+ * atomic stores and passes the shared conformance suite.
45
+ */
46
+ export interface BrowserExperimentMobileDurabilityHost {
47
+ readonly capability: Readonly<{
48
+ implementation: "mobile-host-provided";
49
+ nativeImplementationClaim: false;
50
+ platform: "mobile";
51
+ transactional: true;
52
+ }>;
53
+ readonly identityStore: BrowserExperimentIdentityStore;
54
+ readonly queue: BrowserExperimentDurableQueue;
55
+ }
56
+ export type IndexedDbBrowserExperimentDurabilityOptions = Readonly<{
57
+ databaseName?: string;
58
+ indexedDB?: IDBFactory;
59
+ }>;
60
+ export declare function createIndexedDbBrowserExperimentDurabilityAdapter(options?: IndexedDbBrowserExperimentDurabilityOptions): BrowserExperimentDurabilityAdapter;
61
+ export declare function createBrowserExperimentDurabilityAdapter(backend: BrowserExperimentTransactionalDurability): BrowserExperimentDurabilityAdapter;
62
+ export declare class TransactionalBrowserExperimentIdentityStore implements BrowserExperimentIdentityStore {
63
+ #private;
64
+ constructor(backend: BrowserExperimentTransactionalDurability);
65
+ load(): Promise<BrowserExperimentIdentityState | undefined>;
66
+ save(state: BrowserExperimentIdentityState): Promise<void>;
67
+ }
68
+ export declare class TransactionalBrowserExperimentDurableQueue implements BrowserExperimentDurableQueue {
69
+ #private;
70
+ constructor(backend: BrowserExperimentTransactionalDurability);
71
+ acknowledge(idempotencyKeys: readonly string[]): Promise<void>;
72
+ append(record: BrowserExperimentQueueRecord, limits: BrowserExperimentQueueLimits): Promise<"duplicate" | "stored">;
73
+ failDelivery(idempotencyKeys: readonly string[], maximumAttempts: number): Promise<void>;
74
+ find(idempotencyKey: string): Promise<BrowserExperimentQueueRecord | undefined>;
75
+ pending(input: {
76
+ limit: number;
77
+ now: string;
78
+ runId: string;
79
+ sessionId: string;
80
+ }): Promise<readonly BrowserExperimentQueueRecord[]>;
81
+ purgeIdentity(pseudonymousUnitToken: string): Promise<number>;
82
+ reserveSequence(sessionId: string, runId: string): Promise<number>;
83
+ snapshot(sessionId: string, runId: string): Promise<BrowserExperimentQueueSnapshot>;
84
+ }
85
+ export {};
@@ -0,0 +1 @@
1
+ import*as e from"./config-experimentation.js";var t={823(t){t.exports=e}},r={};function n(e){var o=r[e];if(void 0!==o)return o.exports;var i=r[e]={exports:{}};return t[e](i,i.exports,n),i.exports}n.d=(e,t,r)=>{var o=(t,r)=>{for(var o in t)n.o(t,o)&&!n.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,[r]:t[o]})};o(t,"get"),o(r,"value")},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var o={},i=n(823);function s(e={}){let t=e.indexedDB??globalThis.indexedDB;if(!t||"function"!=typeof t.open)throw x("CONFIG_EXPERIMENT_INDEXEDDB_UNAVAILABLE","Browser experiment durability requires IndexedDB and is unavailable in SSR or this host.");let r=e.databaseName??"runbound-config-experiment-evidence-v1";if(!/^[A-Za-z0-9._-]{1,128}$/u.test(r))throw x("CONFIG_EXPERIMENT_INDEXEDDB_NAME_INVALID","Browser experiment IndexedDB database name is invalid.");return a(new u(t,r))}function a(e){let t=new d(e),r=new c(e);return Object.freeze({capability:Object.freeze({implementation:"indexeddb",nativeImplementationClaim:!1,platform:"web",transactional:!0}),close:()=>e.close?.(),async diagnostics(e,t){w(e,"sessionId"),w(t,"runId");let n=await r.snapshot(e,t);return Object.freeze({authorizationEligible:!1,causalEvidenceOnly:!0,lostRecords:n.lostRecords,pendingRecords:n.pendingRecords,platform:"web-indexeddb",rawIdentifiersIncluded:!1,supported:!0})},identityStore:t,queue:r})}class d{#e;constructor(e){this.#e=e}async load(){return this.#e.transact(e=>({result:e.identity?h(e.identity):void 0,state:e}))}async save(e){p(e),await this.#e.transact(t=>({result:void 0,state:D({...t,identity:h(e)})}))}}class c{#e;constructor(e){this.#e=e}async acknowledge(e){let t=b(e);await this.#e.transact(e=>({result:void 0,state:E(e,e=>t.has(e.record.idempotencyKey)&&"pending"===e.outcome?{outcome:"acknowledged",record:e.record}:e)}))}async append(e,t){m(e),function(e){if(!Number.isSafeInteger(e.maximumPendingRecords)||e.maximumPendingRecords<1||!Number.isSafeInteger(e.maximumRecordBytes)||e.maximumRecordBytes<1)throw x("CONFIG_EXPERIMENT_QUEUE_LIMIT_INVALID","Browser experiment queue limits must be positive safe integers.")}(t);let r=await this.#e.transact(r=>{var n;let o=r.records.find(({record:t})=>t.idempotencyKey===e.idempotencyKey);if(o)return f(o.record)!==f(e)?{result:"conflict",state:r}:{result:"duplicate",state:r};if(n=e,new TextEncoder().encode(_(n)).byteLength>t.maximumRecordBytes)return{result:"oversize",state:I(r,e)};let i=r;for(;i.records.filter(({outcome:e})=>"pending"===e).length>=t.maximumPendingRecords;){let e=y(i.records)[0];if(!e)break;i=I(E(i,t=>t.record.idempotencyKey===e.record.idempotencyKey?{outcome:"lost",record:t.record}:t),e.record)}return{result:"stored",state:D({...i,records:[...i.records,{outcome:"pending",record:h(e)}]})}});if("conflict"===r)throw x("CONFIG_EXPERIMENT_IDEMPOTENCY_CONFLICT","An experiment idempotency key cannot bind different semantic content.");if("oversize"===r)throw x("CONFIG_EXPERIMENT_RECORD_BOUND_EXCEEDED","Browser experiment record exceeds its frozen byte bound.");return r}async failDelivery(e,t){if(!Number.isSafeInteger(t)||t<1)throw x("CONFIG_EXPERIMENT_DELIVERY_BOUND_INVALID","Maximum experiment delivery attempts must be a positive safe integer.");let r=b(e);await this.#e.transact(e=>{let n=e;for(let e of r){let r=n.records.find(({record:t})=>t.idempotencyKey===e);if(r?.outcome!=="pending")continue;let o=r.record.attempts+1;n=E(n,r=>r.record.idempotencyKey===e?{outcome:o>=t?"lost":"pending",record:h({...r.record,attempts:o})}:r),o>=t&&(n=I(n,r.record))}return{result:void 0,state:n}})}async find(e){return w(e,"idempotencyKey"),this.#e.transact(t=>({result:h(t.records.find(({record:t})=>t.idempotencyKey===e)?.record),state:t}))}async pending(e){if(w(e.runId,"runId"),w(e.sessionId,"sessionId"),!Number.isSafeInteger(e.limit)||e.limit<1)throw x("CONFIG_EXPERIMENT_PENDING_BOUND_INVALID","Pending experiment delivery limit must be a positive safe integer.");let t=Date.parse(e.now);if(!Number.isFinite(t))throw x("CONFIG_EXPERIMENT_PENDING_TIME_INVALID","Pending experiment query time must be ISO-8601.");return this.#e.transact(r=>{let n=r;for(let e of r.records)"pending"===e.outcome&&Date.parse(e.record.expiresAt)<=t&&(n=I(E(n,t=>t.record.idempotencyKey===e.record.idempotencyKey?{outcome:"lost",record:t.record}:t),e.record));return{result:Object.freeze(y((n=D({...n,records:n.records.filter(({outcome:e,record:r})=>"pending"===e||Date.parse(r.expiresAt)>t)})).records).filter(({record:t})=>t.sessionId===e.sessionId&&t.runId===e.runId).slice(0,e.limit).map(({record:e})=>h(e))),state:n}})}async purgeIdentity(e){return w(e,"pseudonymousUnitToken"),this.#e.transact(t=>({result:t.records.filter(({record:t})=>t.pseudonymousUnitToken===e).length,state:D({...t,records:t.records.filter(({record:t})=>t.pseudonymousUnitToken!==e)})}))}async reserveSequence(e,t){w(e,"sessionId"),w(t,"runId");let r=N(e,t);return this.#e.transact(e=>{let t=(e.sequences[r]??0)+1;if(!Number.isSafeInteger(t))throw x("CONFIG_EXPERIMENT_SEQUENCE_EXHAUSTED","Browser experiment sequence authority is exhausted.");return{result:t,state:D({...e,sequences:{...e.sequences,[r]:t}})}})}async snapshot(e,t){w(e,"sessionId"),w(t,"runId");let r=N(e,t);return this.#e.transact(n=>({result:Object.freeze({lostRecords:n.lostByScope[r]??0,pendingRecords:n.records.filter(({outcome:r,record:n})=>"pending"===r&&n.sessionId===e&&n.runId===t).length}),state:n}))}}class u{#t;#r;#n;#o;constructor(e,t){this.#r=e,this.#t=t}close(){this.#n?.close(),this.#n=void 0,this.#o=void 0}async transact(e){let t=await this.#i();return new Promise((r,n)=>{let o,i=!1,s=t.transaction("authority","readwrite"),a=s.objectStore("authority"),d=a.get("state");function c(e){i||(i=!0,n(e instanceof Error?e:x("CONFIG_EXPERIMENT_INDEXEDDB_TRANSACTION_FAILED","Browser experiment IndexedDB transaction failed closed.")))}d.onerror=()=>c(d.error),d.onsuccess=()=>{try{let t=function(e){if(void 0===e)return D({formatVersion:"runbound.browser.experiment-durability-state.v1",lostByScope:{},records:[],sequences:{}});if("object"!=typeof e||null===e||!("state"in e))throw x("CONFIG_EXPERIMENT_INDEXEDDB_STATE_INVALID","Browser experiment IndexedDB state is malformed.");let t=e.state;return l(t),D(h(t))}(d.result),r=e(t);l(r.state),o=r.result,a.put({key:"state",state:r.state}),s.commit?.()}catch(e){c(e),s.abort()}},s.onabort=()=>c(s.error??x("CONFIG_EXPERIMENT_INDEXEDDB_TRANSACTION_ABORTED","Browser experiment IndexedDB transaction aborted.")),s.onerror=()=>c(s.error),s.oncomplete=()=>{i||(i=!0,r(o))}})}async #i(){return this.#n?this.#n:(this.#o||(this.#o=new Promise((e,t)=>{let r=this.#r.open(this.#t,1);r.onupgradeneeded=()=>{let e=r.result;e.objectStoreNames.contains("authority")||e.createObjectStore("authority",{keyPath:"key"})},r.onerror=()=>t(r.error),r.onblocked=()=>t(x("CONFIG_EXPERIMENT_INDEXEDDB_UPGRADE_BLOCKED","Browser experiment IndexedDB upgrade is blocked by another context.")),r.onsuccess=()=>{let t=r.result;t.onversionchange=()=>{t.close(),this.#n=void 0,this.#o=void 0},this.#n=t,e(t)}}).catch(e=>{throw this.#o=void 0,e instanceof Error?e:x("CONFIG_EXPERIMENT_INDEXEDDB_OPEN_FAILED","Browser experiment IndexedDB authority failed to open.")})),this.#o)}}function l(e){if("runbound.browser.experiment-durability-state.v1"!==e.formatVersion||!Array.isArray(e.records)||"object"!=typeof e.lostByScope||null===e.lostByScope||"object"!=typeof e.sequences||null===e.sequences)throw x("CONFIG_EXPERIMENT_INDEXEDDB_STATE_INVALID","Browser experiment IndexedDB state does not match its durable contract.");e.identity&&p(e.identity);let t=new Set;for(let r of e.records){if(!["acknowledged","lost","pending"].includes(r.outcome)||t.has(r.record.idempotencyKey))throw x("CONFIG_EXPERIMENT_INDEXEDDB_STATE_INVALID","Browser experiment IndexedDB record authority is inconsistent.");t.add(r.record.idempotencyKey),m(r.record)}for(let t of[...Object.values(e.lostByScope),...Object.values(e.sequences)])if(!Number.isSafeInteger(t)||t<0)throw x("CONFIG_EXPERIMENT_INDEXEDDB_STATE_INVALID","Browser experiment IndexedDB counters are invalid.")}function p(e){if(!Number.isSafeInteger(e.generation)||e.generation<0||"active"!==e.state&&"deleted"!==e.state)throw x("CONFIG_EXPERIMENT_IDENTITY_STATE_INVALID","Browser experiment identity state is invalid.");if("active"===e.state){if("granted"!==e.consent||!0!==e.firstParty||!Array.isArray(e.priorPseudonymousUnitTokens))throw x("CONFIG_EXPERIMENT_IDENTITY_STATE_INVALID","Browser experiment identity must remain consented, first-party, and pseudonymous.");for(let t of(w(e.pseudonymousUnitToken,"pseudonymousUnitToken"),e.priorPseudonymousUnitTokens))w(t,"priorPseudonymousUnitToken")}}function m(e){for(let[t,r]of[["idempotencyKey",e.idempotencyKey],["pseudonymousUnitToken",e.pseudonymousUnitToken],["runId",e.runId],["sessionId",e.sessionId]])w(r,t);if(!Number.isSafeInteger(e.sequence)||e.sequence<1||!Number.isSafeInteger(e.attempts)||e.attempts<0||!Number.isSafeInteger(e.identityGeneration)||e.identityGeneration<1||!Number.isFinite(Date.parse(e.createdAt))||!Number.isFinite(Date.parse(e.expiresAt))||Date.parse(e.expiresAt)<=Date.parse(e.createdAt)||!1!==e.evidencePosture.authorizationEligible||"causalEvidenceOnly"!==e.evidencePosture.evidencePurpose||0!==e.evidencePosture.authorizationCapabilities.length)throw x("CONFIG_EXPERIMENT_QUEUE_RECORD_INVALID","Browser experiment queue record violates its causal-only durable contract.")}function I(e,t){let r=N(t.sessionId,t.runId);return D({...e,lostByScope:{...e.lostByScope,[r]:(e.lostByScope[r]??0)+1}})}function E(e,t){return D({...e,records:e.records.map(t)})}function y(e){return e.filter(({outcome:e})=>"pending"===e).sort((e,t)=>e.record.createdAt.localeCompare(t.record.createdAt)||e.record.sequence-t.record.sequence||e.record.idempotencyKey.localeCompare(t.record.idempotencyKey))}function f(e){return _({idempotencyKey:e.idempotencyKey,identityGeneration:e.identityGeneration,kind:e.kind,payload:e.payload,pseudonymousUnitToken:e.pseudonymousUnitToken,runId:e.runId,sessionId:e.sessionId})}function b(e){let t=new Set;for(let r of e)w(r,"idempotencyKey"),t.add(r);return t}function N(e,t){return`${e}\u0000${t}`}function _(e){return JSON.stringify(function e(t){if(null===t||"boolean"==typeof t||"string"==typeof t)return t;if("number"==typeof t){if(!Number.isFinite(t))throw x("CONFIG_EXPERIMENT_QUEUE_RECORD_INVALID","Browser experiment queue record contains a non-finite number.");return Object.is(t,-0)?0:t}if(Array.isArray(t))return t.map(e);if("object"!=typeof t||void 0===t)throw x("CONFIG_EXPERIMENT_QUEUE_RECORD_INVALID","Browser experiment queue record is not canonical JSON.");return Object.fromEntries(Object.entries(t).filter(([,e])=>void 0!==e).sort(([e],[t])=>e.localeCompare(t)).map(([t,r])=>[t,e(r)]))}(e))}function D(e){return function e(t){if(t&&"object"==typeof t&&!Object.isFrozen(t))for(let r of(Object.freeze(t),Object.values(t)))e(r);return t}(h(e))}function h(e){return void 0===e?e:structuredClone(e)}function w(e,t){if("string"!=typeof e||e.length<1||e.length>512||[...e].some(e=>{let t=e.codePointAt(0)??0;return t<32||127===t}))throw x("CONFIG_EXPERIMENT_DURABILITY_TOKEN_INVALID",`Browser experiment ${t} is invalid.`)}function x(e,t){return new i.BrowserExperimentRuntimeError(e,t)}n.d(o,{Ac:()=>a,JT:()=>s,XU:()=>c,kl:()=>d});var T=o.XU,B=o.kl,g=o.Ac,O=o.JT;export{T as TransactionalBrowserExperimentDurableQueue,B as TransactionalBrowserExperimentIdentityStore,g as createBrowserExperimentDurabilityAdapter,O as createIndexedDbBrowserExperimentDurabilityAdapter};
@@ -0,0 +1,22 @@
1
+ import type { BrowserExperimentAssignmentProof, BrowserExperimentProofVerifier, BrowserExperimentSemanticIdentity } from "./config-experimentation.js";
2
+ export type BrowserExperimentWebCryptoAlgorithm = "ECDSA_P256_SHA256" | "Ed25519";
3
+ export type BrowserExperimentTrustedKey = Readonly<{
4
+ algorithm: BrowserExperimentWebCryptoAlgorithm;
5
+ key: CryptoKey;
6
+ keyId: string;
7
+ purpose: "ingestion" | "server-assignment";
8
+ }>;
9
+ export type BrowserExperimentWebCryptoVerifierOptions = Readonly<{
10
+ crypto?: Crypto;
11
+ localIngestionKeyId: string;
12
+ trustedKeys: readonly BrowserExperimentTrustedKey[];
13
+ }>;
14
+ export type BrowserExperimentProofMessagePurpose = "ingestion" | "server-assignment";
15
+ export declare function createWebCryptoBrowserExperimentProofVerifier(options: BrowserExperimentWebCryptoVerifierOptions): BrowserExperimentProofVerifier;
16
+ /** Canonical signing bytes shared by trusted receipt issuers and Web clients. */
17
+ export declare function browserExperimentProofMessageBytes(input: Readonly<{
18
+ proof: BrowserExperimentAssignmentProof;
19
+ purpose: BrowserExperimentProofMessagePurpose;
20
+ semanticIdentity: BrowserExperimentSemanticIdentity;
21
+ }>): Uint8Array<ArrayBuffer>;
22
+ export declare function encodeBrowserExperimentCompactReceipt(keyId: string, signature: Uint8Array): string;
@@ -0,0 +1 @@
1
+ import*as e from"./config-experimentation.js";var t={823(t){t.exports=e}},r={};function n(e){var i=r[e];if(void 0!==i)return i.exports;var o=r[e]={exports:{}};return t[e](o,o.exports,n),o.exports}n.d=(e,t,r)=>{var i=(t,r)=>{for(var i in t)n.o(t,i)&&!n.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,[r]:t[i]})};i(t,"get"),i(r,"value")},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var i={},o=n(823);function s(e){let t=e.crypto??globalThis.crypto;if(!t?.subtle||"function"!=typeof t.subtle.verify)throw f("CONFIG_EXPERIMENT_WEBCRYPTO_UNAVAILABLE","Trusted browser experiment proof verification requires WebCrypto and is unavailable in SSR or this host.");u(e.localIngestionKeyId,"localIngestionKeyId");let r=new Map;for(let t of e.trustedKeys){u(t.keyId,"keyId");let e=`${t.purpose}\u0000${t.keyId}`;if(r.has(e))throw f("CONFIG_EXPERIMENT_WEBCRYPTO_KEY_CONFLICT","Trusted browser experiment proof keys must be unique by purpose and key ID.");r.set(e,Object.freeze({...t}))}async function n(e,n,i,o,s){try{var c;let I=r.get(`${e}\u0000${n}`);if(!I||I.purpose!==e)return!1;let u=a({proof:o,purpose:e,semanticIdentity:s});return await t.subtle.verify((c=I.algorithm,"Ed25519"===c?{name:"Ed25519"}:{hash:"SHA-256",name:"ECDSA"}),I.key,p(i),u)}catch{return!1}}return Object.freeze({verifyLocalArtifact:async(t,r)=>n("ingestion",e.localIngestionKeyId,t.trustedIngestionSignature,t,r),verifyServerAssignment:async(e,t)=>n("server-assignment",e.keyId,e.signature,e,t),async verifyTrustedIngestionReceipt(t,r){if("localArtifact"===t.kind)return n("ingestion",e.localIngestionKeyId,t.trustedIngestionSignature,t,r);let i=function(e){let t=e.indexOf(".");if(t<1||t!==e.lastIndexOf("."))return;let r=e.slice(0,t),n=e.slice(t+1);try{return u(r,"receiptKeyId"),p(n),Object.freeze({keyId:r,signature:n})}catch{return}}(t.trustedIngestionReceipt);return!!i&&n("ingestion",i.keyId,i.signature,t,r)}})}function a(e){var t=e.semanticIdentity;if(!Number.isSafeInteger(t.assignmentEpoch)||t.assignmentEpoch<1)throw f("CONFIG_EXPERIMENT_PROOF_IDENTITY_INVALID","Browser experiment proof assignment epoch is invalid.");for(let[e,r]of[["assignmentUnitToken",t.assignmentUnitToken],["iterationId",t.iterationId],["runId",t.runId],["semanticBoundaryId",t.semanticBoundaryId],["switchbackPeriodId",t.switchbackPeriodId]])void 0!==r&&u(r,e);let r=function(e){if("serverSigned"===e.kind)return u(e.assignmentToken,"assignmentToken"),u(e.keyId,"keyId"),{assignmentToken:e.assignmentToken,keyId:e.keyId,kind:e.kind};if(u(e.activationSetId,"activationSetId"),u(e.trustedIngestionReceipt,"trustedIngestionReceipt"),!/^sha256:[0-9a-f]{64}$/u.test(e.artifactDigest)||!Number.isSafeInteger(e.bucket)||e.bucket<0)throw f("CONFIG_EXPERIMENT_LOCAL_ARTIFACT_PROOF_INVALID","Local browser experiment artifact proof is invalid.");return{activationSetId:e.activationSetId,artifactDigest:e.artifactDigest,bucket:e.bucket,kind:e.kind,trustedIngestionReceipt:e.trustedIngestionReceipt}}(e.proof);if("server-assignment"===e.purpose&&"serverSigned"!==e.proof.kind)throw f("CONFIG_EXPERIMENT_PROOF_PURPOSE_MISMATCH","Server-assignment proof bytes require a server-signed proof.");return new TextEncoder().encode(JSON.stringify(function e(t){if(null===t||"boolean"==typeof t||"string"==typeof t)return t;if("number"==typeof t){if(!Number.isFinite(t))throw f("CONFIG_EXPERIMENT_PROOF_MESSAGE_INVALID","Browser experiment proof message contains a non-finite number.");return Object.is(t,-0)?0:t}if(Array.isArray(t))return t.map(e);if("object"!=typeof t||void 0===t)throw f("CONFIG_EXPERIMENT_PROOF_MESSAGE_INVALID","Browser experiment proof message is not canonical JSON.");return Object.fromEntries(Object.entries(t).filter(([,e])=>void 0!==e).sort(([e],[t])=>e.localeCompare(t)).map(([t,r])=>[t,e(r)]))}({domain:"runbound.browser.config-experiment-proof.v1",proof:r,purpose:e.purpose,semanticIdentity:e.semanticIdentity,semanticIdempotencyKey:I(e.semanticIdentity),transportIdempotencyKey:I(e.semanticIdentity)})))}function c(e,t){if(u(e,"receiptKeyId"),t.byteLength<32||t.byteLength>512)throw f("CONFIG_EXPERIMENT_RECEIPT_SIGNATURE_INVALID","Trusted ingestion receipt signature length is invalid.");return`${e}.${function(e){let t="";for(let r of e)t+=String.fromCharCode(r);return globalThis.btoa(t).replaceAll("+","-").replaceAll("/","_").replace(/=+$/u,"")}(t)}`}function I(e){return["assignment",e.runId,e.iterationId,e.assignmentEpoch,e.assignmentUnitToken,e.semanticBoundaryId,e.switchbackPeriodId??""].join(":")}function p(e){if(!/^[A-Za-z0-9_-]+$/u.test(e))throw f("CONFIG_EXPERIMENT_SIGNATURE_ENCODING_INVALID","Browser experiment proof signature is not base64url.");let t=e.replaceAll("-","+").replaceAll("_","/"),r=globalThis.atob(t.padEnd(4*Math.ceil(t.length/4),"=")),n=new Uint8Array(r.length);for(let e=0;e<r.length;e+=1)n[e]=r.charCodeAt(e);if(n.byteLength<32||n.byteLength>512)throw f("CONFIG_EXPERIMENT_SIGNATURE_ENCODING_INVALID","Browser experiment proof signature length is invalid.");return n}function u(e,t){if("string"!=typeof e||e.length<1||e.length>512||[...e].some(e=>{let t=e.codePointAt(0)??0;return t<32||127===t}))throw f("CONFIG_EXPERIMENT_PROOF_TOKEN_INVALID",`Browser experiment proof ${t} is invalid.`)}function f(e,t){return new o.BrowserExperimentRuntimeError(e,t)}n.d(i,{Fi:()=>s,Zy:()=>c,cj:()=>a});var d=i.cj,l=i.Fi,g=i.Zy;export{d as browserExperimentProofMessageBytes,l as createWebCryptoBrowserExperimentProofVerifier,g as encodeBrowserExperimentCompactReceipt};
@@ -0,0 +1,308 @@
1
+ import type { ConfigExperimentObservationEvent } from "./internal/config-contract/experimentation.js";
2
+ export type BrowserExperimentEvidenceStrength = "advisory" | "confirmatory" | "exploratory";
3
+ export type BrowserExperimentJson = boolean | null | number | string | readonly BrowserExperimentJson[] | {
4
+ readonly [key: string]: BrowserExperimentJson;
5
+ };
6
+ export type BrowserExperimentQaOverride = Readonly<{
7
+ exclusionPolicy: "excludeFromConfirmatoryAndWinnerSelection";
8
+ overrideId: string;
9
+ }>;
10
+ export type BrowserExperimentSemanticIdentity = Readonly<{
11
+ assignmentEpoch: number;
12
+ assignmentUnitToken: string;
13
+ iterationId: string;
14
+ qaOverride?: BrowserExperimentQaOverride;
15
+ runId: string;
16
+ semanticBoundaryId: string;
17
+ switchbackPeriodId?: string;
18
+ }>;
19
+ export type BrowserExperimentAssignmentProof = Readonly<{
20
+ assignmentToken: string;
21
+ keyId: string;
22
+ kind: "serverSigned";
23
+ signature: string;
24
+ trustedIngestionReceipt: string;
25
+ }> | Readonly<{
26
+ activationSetId: string;
27
+ artifactDigest: string;
28
+ bucket: number;
29
+ kind: "localArtifact";
30
+ trustedIngestionReceipt: string;
31
+ trustedIngestionSignature: string;
32
+ }>;
33
+ export type BrowserExperimentFrozenThresholds = Readonly<{
34
+ maximumClockSkewMilliseconds: number;
35
+ maximumIdentityTransitionsPerRun: number;
36
+ maximumLossRate: number;
37
+ maximumMissingnessRate: number;
38
+ requireHumanTraffic: true;
39
+ requireUnblockedTransport: true;
40
+ revision: string;
41
+ }>;
42
+ export type BrowserExperimentMeasurement = Readonly<{
43
+ adBlockerDetected: boolean;
44
+ automatedTraffic: "human" | "knownBot" | "unknown";
45
+ clockSkewMilliseconds: number;
46
+ missingRecords: number;
47
+ observedRecords: number;
48
+ transportBlocked: boolean;
49
+ }>;
50
+ export type BrowserExperimentEvidencePosture = Readonly<{
51
+ authorizationCapabilities: readonly never[];
52
+ authorizationEligible: false;
53
+ blockingReasons: readonly string[];
54
+ evidencePurpose: "causalEvidenceOnly";
55
+ evidenceStrength: BrowserExperimentEvidenceStrength;
56
+ frozenThresholdRevision: string;
57
+ humanExposureClaim: false;
58
+ renderExposureClaim: false;
59
+ }>;
60
+ export type BrowserExperimentActiveIdentity = Readonly<{
61
+ boundAt: string;
62
+ consent: "granted";
63
+ firstParty: true;
64
+ generation: number;
65
+ priorPseudonymousUnitTokens: readonly string[];
66
+ pseudonymousUnitToken: string;
67
+ state: "active";
68
+ transitionCount: number;
69
+ }>;
70
+ export type BrowserExperimentIdentityTombstone = Readonly<{
71
+ deletedAt: string;
72
+ deletionId: string;
73
+ generation: number;
74
+ state: "deleted";
75
+ }>;
76
+ export type BrowserExperimentIdentityState = BrowserExperimentActiveIdentity | BrowserExperimentIdentityTombstone;
77
+ export interface BrowserExperimentIdentityStore {
78
+ load(): Promise<BrowserExperimentIdentityState | undefined>;
79
+ save(state: BrowserExperimentIdentityState): Promise<void>;
80
+ }
81
+ export type BrowserExperimentRecordKind = "assignment" | "exposure" | "observation";
82
+ export type BrowserExperimentObservationValueProvenance = Readonly<{
83
+ metricRevisionId: string;
84
+ sourceRevision: string;
85
+ valueField: string;
86
+ }>;
87
+ export type BrowserExperimentQueueRecord = Readonly<{
88
+ attempts: number;
89
+ createdAt: string;
90
+ evidencePosture: BrowserExperimentEvidencePosture;
91
+ expiresAt: string;
92
+ idempotencyKey: string;
93
+ identityGeneration: number;
94
+ kind: BrowserExperimentRecordKind;
95
+ payload: BrowserExperimentJson;
96
+ pseudonymousUnitToken: string;
97
+ runId: string;
98
+ sequence: number;
99
+ sessionId: string;
100
+ }>;
101
+ export type BrowserExperimentQueueLimits = Readonly<{
102
+ maximumPendingRecords: number;
103
+ maximumRecordBytes: number;
104
+ }>;
105
+ export type BrowserExperimentQueueSnapshot = Readonly<{
106
+ lostRecords: number;
107
+ pendingRecords: number;
108
+ }>;
109
+ /**
110
+ * Production clients inject a persistent implementation whose append,
111
+ * sequence reservation, acknowledgement, attempt and loss updates are atomic.
112
+ * `find` covers pending and acknowledged idempotency tombstones so browser
113
+ * retries never manufacture a second transport identity.
114
+ */
115
+ export interface BrowserExperimentDurableQueue {
116
+ acknowledge(idempotencyKeys: readonly string[]): Promise<void>;
117
+ append(record: BrowserExperimentQueueRecord, limits: BrowserExperimentQueueLimits): Promise<"duplicate" | "stored">;
118
+ failDelivery(idempotencyKeys: readonly string[], maximumAttempts: number): Promise<void>;
119
+ find(idempotencyKey: string): Promise<BrowserExperimentQueueRecord | undefined>;
120
+ pending(input: {
121
+ limit: number;
122
+ now: string;
123
+ runId: string;
124
+ sessionId: string;
125
+ }): Promise<readonly BrowserExperimentQueueRecord[]>;
126
+ purgeIdentity(pseudonymousUnitToken: string): Promise<number>;
127
+ reserveSequence(sessionId: string, runId: string): Promise<number>;
128
+ snapshot(sessionId: string, runId: string): Promise<BrowserExperimentQueueSnapshot>;
129
+ }
130
+ export interface BrowserExperimentProofVerifier {
131
+ verifyLocalArtifact(proof: Extract<BrowserExperimentAssignmentProof, {
132
+ kind: "localArtifact";
133
+ }>, semanticIdentity: BrowserExperimentSemanticIdentity): Promise<boolean>;
134
+ verifyServerAssignment(proof: Extract<BrowserExperimentAssignmentProof, {
135
+ kind: "serverSigned";
136
+ }>, semanticIdentity: BrowserExperimentSemanticIdentity): Promise<boolean>;
137
+ verifyTrustedIngestionReceipt(proof: BrowserExperimentAssignmentProof, semanticIdentity: BrowserExperimentSemanticIdentity): Promise<boolean>;
138
+ }
139
+ export type BrowserExperimentRuntimeBounds = Readonly<{
140
+ maximumBackgroundFlushEvents: number;
141
+ maximumBackgroundFlushMilliseconds: number;
142
+ maximumDeliveryAttempts: number;
143
+ maximumForegroundFlushEvents: number;
144
+ maximumPendingRecords: number;
145
+ maximumRecordBytes: number;
146
+ recordTtlMilliseconds: number;
147
+ }>;
148
+ export type BrowserExperimentAssignmentReceipt = Readonly<{
149
+ armId: string;
150
+ evidencePosture: BrowserExperimentEvidencePosture;
151
+ exposureToken: string;
152
+ identityGeneration: number;
153
+ record: BrowserExperimentQueueRecord;
154
+ semanticIdentity: BrowserExperimentSemanticIdentity;
155
+ status: "assigned";
156
+ }>;
157
+ export type BrowserExperimentExposureReceipt = Readonly<{
158
+ evidencePosture: BrowserExperimentEvidencePosture;
159
+ exposureRecorded: true;
160
+ humanExposureClaim: false;
161
+ record: BrowserExperimentQueueRecord;
162
+ renderExposureClaim: false;
163
+ status: "exposed";
164
+ }>;
165
+ export type BrowserExperimentObservationEventFactoryInput = Readonly<{
166
+ analysisUnitToken: string;
167
+ assignment: BrowserExperimentAssignmentReceipt;
168
+ dimensionValues: Readonly<Record<string, boolean | number | string>>;
169
+ metricRevisionId: string;
170
+ observationId: string;
171
+ occurredAt: string;
172
+ transport: Readonly<{
173
+ eventId: string;
174
+ producerEpoch: string;
175
+ producerId: string;
176
+ producerSequence: string;
177
+ producerSessionId: string;
178
+ }>;
179
+ value: boolean | number;
180
+ valueProvenance: BrowserExperimentObservationValueProvenance;
181
+ }>;
182
+ export type BrowserExperimentObservationReceipt = Readonly<{
183
+ authorizationCapabilities: readonly never[];
184
+ authorizationEligible: false;
185
+ evidencePosture: BrowserExperimentEvidencePosture;
186
+ event: ConfigExperimentObservationEvent;
187
+ record: BrowserExperimentQueueRecord;
188
+ status: "observed";
189
+ valueProvenance: BrowserExperimentObservationValueProvenance;
190
+ }>;
191
+ export type BrowserExperimentDeliveryBatch = Readonly<{
192
+ records: readonly BrowserExperimentQueueRecord[];
193
+ runId: string;
194
+ sessionId: string;
195
+ }>;
196
+ export type BrowserExperimentDeliveryResult = Readonly<{
197
+ acceptedIdempotencyKeys: readonly string[];
198
+ }>;
199
+ export type BrowserExperimentFlushResult = Readonly<{
200
+ acknowledged: number;
201
+ attempted: number;
202
+ background: boolean;
203
+ offline: boolean;
204
+ pending: number;
205
+ timedOut: boolean;
206
+ }>;
207
+ export declare class BrowserExperimentRuntimeError extends Error {
208
+ readonly code: string;
209
+ constructor(code: string, message: string);
210
+ }
211
+ export declare class ConfigExperimentBrowserRuntime {
212
+ #private;
213
+ constructor(input: {
214
+ bounds: BrowserExperimentRuntimeBounds;
215
+ identityStore: BrowserExperimentIdentityStore;
216
+ proofVerifier: BrowserExperimentProofVerifier;
217
+ queue: BrowserExperimentDurableQueue;
218
+ thresholds: BrowserExperimentFrozenThresholds;
219
+ });
220
+ bindIdentity(input: {
221
+ consent: "denied" | "granted" | "unknown";
222
+ firstParty: boolean;
223
+ occurredAt: string;
224
+ pseudonymousUnitToken: string;
225
+ }): Promise<BrowserExperimentActiveIdentity>;
226
+ rotateIdentity(input: {
227
+ consent: "denied" | "granted" | "unknown";
228
+ firstParty: boolean;
229
+ nextPseudonymousUnitToken: string;
230
+ occurredAt: string;
231
+ }): Promise<BrowserExperimentActiveIdentity>;
232
+ deleteIdentity(input: {
233
+ deletedAt: string;
234
+ deletionId: string;
235
+ }): Promise<Readonly<{
236
+ purgedRecords: number;
237
+ tombstone: BrowserExperimentIdentityTombstone;
238
+ }>>;
239
+ recordAssignment(input: {
240
+ armId: string;
241
+ exposureToken: string;
242
+ measurement: BrowserExperimentMeasurement;
243
+ occurredAt: string;
244
+ proof: BrowserExperimentAssignmentProof;
245
+ semanticIdentity: BrowserExperimentSemanticIdentity;
246
+ sessionId: string;
247
+ }): Promise<BrowserExperimentAssignmentReceipt>;
248
+ expose(input: {
249
+ assignment: BrowserExperimentAssignmentReceipt;
250
+ measurement: BrowserExperimentMeasurement;
251
+ occurredAt: string;
252
+ semanticBoundaryId: string;
253
+ sessionId: string;
254
+ }): Promise<BrowserExperimentExposureReceipt>;
255
+ observe(input: {
256
+ allowedDimensionKeys: readonly string[];
257
+ assignment: BrowserExperimentAssignmentReceipt;
258
+ createObservationEvent: (input: BrowserExperimentObservationEventFactoryInput) => ConfigExperimentObservationEvent;
259
+ dimensionValues: Readonly<Record<string, boolean | number | string>>;
260
+ measurement: BrowserExperimentMeasurement;
261
+ metricRevisionId: string;
262
+ observationId: string;
263
+ occurredAt: string;
264
+ sessionId: string;
265
+ value: boolean | number;
266
+ valueProvenance: Omit<BrowserExperimentObservationValueProvenance, "metricRevisionId">;
267
+ }): Promise<BrowserExperimentObservationReceipt>;
268
+ flush(input: {
269
+ background: boolean;
270
+ deliver(batch: BrowserExperimentDeliveryBatch): Promise<BrowserExperimentDeliveryResult>;
271
+ now: string;
272
+ offline: boolean;
273
+ runId: string;
274
+ sessionId: string;
275
+ }): Promise<BrowserExperimentFlushResult>;
276
+ }
277
+ export declare function assessBrowserExperimentEvidence(input: {
278
+ identity: BrowserExperimentActiveIdentity;
279
+ identityTransitionAfterAssignment: boolean;
280
+ ingestionVerified: boolean;
281
+ measurement: BrowserExperimentMeasurement;
282
+ proofVerified: boolean;
283
+ queueSnapshot: BrowserExperimentQueueSnapshot;
284
+ thresholds: BrowserExperimentFrozenThresholds;
285
+ }): BrowserExperimentEvidencePosture;
286
+ /** Test/reference store only; production clients inject persistent storage. */
287
+ export declare class MemoryBrowserExperimentIdentityStore implements BrowserExperimentIdentityStore {
288
+ #private;
289
+ load(): Promise<BrowserExperimentIdentityState | undefined>;
290
+ save(state: BrowserExperimentIdentityState): Promise<void>;
291
+ }
292
+ /** Test/reference queue only; production clients inject persistent storage. */
293
+ export declare class MemoryBrowserExperimentDurableQueue implements BrowserExperimentDurableQueue {
294
+ #private;
295
+ acknowledge(idempotencyKeys: readonly string[]): Promise<void>;
296
+ append(record: BrowserExperimentQueueRecord, limits: BrowserExperimentQueueLimits): Promise<"duplicate" | "stored">;
297
+ failDelivery(idempotencyKeys: readonly string[], maximumAttempts: number): Promise<void>;
298
+ find(idempotencyKey: string): Promise<BrowserExperimentQueueRecord | undefined>;
299
+ pending(input: {
300
+ limit: number;
301
+ now: string;
302
+ runId: string;
303
+ sessionId: string;
304
+ }): Promise<readonly BrowserExperimentQueueRecord[]>;
305
+ purgeIdentity(pseudonymousUnitToken: string): Promise<number>;
306
+ reserveSequence(sessionId: string, runId: string): Promise<number>;
307
+ snapshot(sessionId: string, runId: string): Promise<BrowserExperimentQueueSnapshot>;
308
+ }
@@ -0,0 +1 @@
1
+ var e={};e.d=(t,n,i)=>{var s=(n,i)=>{for(var s in n)e.o(n,s)&&!e.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,[i]:n[s]})};s(n,"get"),s(i,"value")},e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var t={};class n extends Error{code;constructor(e,t){super(t),this.name="BrowserExperimentRuntimeError",this.code=e}}class i{#e;#t;#n;#i;#s;constructor(e){(function(e){if([e.maximumBackgroundFlushEvents,e.maximumBackgroundFlushMilliseconds,e.maximumDeliveryAttempts,e.maximumForegroundFlushEvents,e.maximumPendingRecords,e.maximumRecordBytes,e.recordTtlMilliseconds].some(e=>!Number.isSafeInteger(e)||e<1))throw w("CONFIG_EXPERIMENT_BOUNDS_INVALID","Browser experiment queue and delivery bounds must be positive safe integers.");if(e.maximumBackgroundFlushEvents>e.maximumForegroundFlushEvents)throw w("CONFIG_EXPERIMENT_BACKGROUND_BOUND_INVALID","Background flushes cannot exceed the foreground event bound.")})(e.bounds),I(e.thresholds),this.#e=Object.freeze({...e.bounds}),this.#t=e.identityStore,this.#n=e.proofVerifier,this.#i=e.queue,this.#s=Object.freeze({...e.thresholds})}async bindIdentity(e){if(b(e.occurredAt,"occurredAt"),T(e.pseudonymousUnitToken,"pseudonymousUnitToken"),"granted"!==e.consent||!e.firstParty)throw w("CONFIG_EXPERIMENT_IDENTITY_CONSENT_REQUIRED","Browser experiment identity requires granted consent and a first-party binding.");let t=await this.#t.load();if(t?.state==="active"){if(t.pseudonymousUnitToken!==e.pseudonymousUnitToken)throw w("CONFIG_EXPERIMENT_IDENTITY_ROTATION_REQUIRED","A different pseudonymous identity must use the explicit rotation lifecycle.");return t}let n=Object.freeze({boundAt:e.occurredAt,consent:"granted",firstParty:!0,generation:(t?.generation??0)+1,priorPseudonymousUnitTokens:Object.freeze([]),pseudonymousUnitToken:e.pseudonymousUnitToken,state:"active",transitionCount:0});return await this.#t.save(n),n}async rotateIdentity(e){b(e.occurredAt,"occurredAt"),T(e.nextPseudonymousUnitToken,"nextPseudonymousUnitToken");let t=await this.#r();if("granted"!==e.consent||!e.firstParty)throw w("CONFIG_EXPERIMENT_IDENTITY_CONSENT_REQUIRED","Identity rotation requires current consent and a first-party binding.");if(t.pseudonymousUnitToken===e.nextPseudonymousUnitToken)return t;if(t.priorPseudonymousUnitTokens.length>=32)throw w("CONFIG_EXPERIMENT_IDENTITY_LINEAGE_BOUND_EXCEEDED","Browser experiment identity rotation exceeded its bounded deletion lineage.");let n=Object.freeze({boundAt:e.occurredAt,consent:"granted",firstParty:!0,generation:t.generation+1,priorPseudonymousUnitTokens:Object.freeze([...t.priorPseudonymousUnitTokens,t.pseudonymousUnitToken]),pseudonymousUnitToken:e.nextPseudonymousUnitToken,state:"active",transitionCount:t.transitionCount+1});return await this.#t.save(n),n}async deleteIdentity(e){b(e.deletedAt,"deletedAt"),T(e.deletionId,"deletionId");let t=await this.#t.load();if(t?.state==="deleted"){if(t.deletionId!==e.deletionId)throw w("CONFIG_EXPERIMENT_DELETION_CONFLICT","Identity deletion is idempotent only for the original deletion identifier.");return Object.freeze({purgedRecords:0,tombstone:t})}let n=0;if(t)for(let e of[...t.priorPseudonymousUnitTokens,t.pseudonymousUnitToken])n+=await this.#i.purgeIdentity(e);let i=Object.freeze({deletedAt:e.deletedAt,deletionId:e.deletionId,generation:t?.generation??0,state:"deleted"});return await this.#t.save(i),Object.freeze({purgedRecords:n,tombstone:i})}async recordAssignment(e){var t,n,i;T(e.armId,"armId"),T(e.exposureToken,"exposureToken"),T(e.sessionId,"sessionId"),b(e.occurredAt,"occurredAt"),function(e){if(!Number.isSafeInteger(e.assignmentEpoch)||e.assignmentEpoch<1)throw w("CONFIG_EXPERIMENT_ASSIGNMENT_EPOCH_INVALID","Assignment epoch must be a positive safe integer.");for(let[t,n]of Object.entries({assignmentUnitToken:e.assignmentUnitToken,iterationId:e.iterationId,runId:e.runId,semanticBoundaryId:e.semanticBoundaryId}))T(n,t);void 0!==e.switchbackPeriodId&&T(e.switchbackPeriodId,"switchbackPeriodId"),void 0!==e.qaOverride&&function(e){if(null===e||"object"!=typeof e||Array.isArray(e)||"exclusionPolicy,overrideId"!==Object.keys(e).sort().join(","))throw w("CONFIG_EXPERIMENT_QA_OVERRIDE_INVALID","QA overrides must be closed explicit evidence markers.");if(T(e.overrideId,"qaOverride.overrideId"),"excludeFromConfirmatoryAndWinnerSelection"!==e.exclusionPolicy)throw w("CONFIG_EXPERIMENT_QA_OVERRIDE_POLICY_INVALID","QA overrides must use the immutable confirmatory-analysis and winner-selection exclusion policy.")}(e.qaOverride)}(e.semanticIdentity),l(e.measurement);let r=await this.#r();if(r.pseudonymousUnitToken!==e.semanticIdentity.assignmentUnitToken)throw w("CONFIG_EXPERIMENT_ASSIGNMENT_IDENTITY_MISMATCH","Assignment evidence must bind the active first-party pseudonymous identity.");let o=(t=e.semanticIdentity,`assignment:${p(t)}`),a=await this.#i.find(o);if(a)return function(e,t){if(R(e.payload)!==R(t))throw w("CONFIG_EXPERIMENT_IDEMPOTENCY_CONFLICT","A semantic assignment retry changed its evidence content.")}(a,{armId:e.armId,exposureToken:e.exposureToken,proof:e.proof,semanticIdentity:e.semanticIdentity}),d(a,e);let u="serverSigned"===(n=e.proof).kind?[n.assignmentToken,n.keyId,n.signature,n.trustedIngestionReceipt].every(_):[n.activationSetId,n.artifactDigest,n.trustedIngestionReceipt,n.trustedIngestionSignature].every(_)&&Number.isSafeInteger(n.bucket)&&n.bucket>=0,c=!!u&&await m(this.#n,e.proof,e.semanticIdentity),I=!!u&&await this.#n.verifyTrustedIngestionReceipt(e.proof,e.semanticIdentity),y=await this.#i.snapshot(e.sessionId,e.semanticIdentity.runId),f=(i=s({identity:r,identityTransitionAfterAssignment:!1,ingestionVerified:I,measurement:e.measurement,proofVerified:c,queueSnapshot:y,thresholds:this.#s}),void 0===e.semanticIdentity.qaOverride?i:Object.freeze({...i,blockingReasons:Object.freeze([...new Set([...i.blockingReasons,"qa-override-excluded"])].sort()),evidenceStrength:"exploratory"})),v=await this.#i.reserveSequence(e.sessionId,e.semanticIdentity.runId),g=h({attempts:0,createdAt:e.occurredAt,evidencePosture:f,expiresAt:E(e.occurredAt,this.#e.recordTtlMilliseconds),idempotencyKey:o,identityGeneration:r.generation,kind:"assignment",payload:{armId:e.armId,exposureToken:e.exposureToken,proof:e.proof,semanticIdentity:e.semanticIdentity},pseudonymousUnitToken:r.pseudonymousUnitToken,runId:e.semanticIdentity.runId,sequence:v,sessionId:e.sessionId});return await this.#i.append(g,this.#o()),d(g,e)}async expose(e){var t,n;if(b(e.occurredAt,"occurredAt"),T(e.semanticBoundaryId,"semanticBoundaryId"),T(e.sessionId,"sessionId"),l(e.measurement),e.assignment.record.sessionId!==e.sessionId)throw w("CONFIG_EXPERIMENT_EXPOSURE_SESSION_MISMATCH","Explicit exposure must remain in the assignment session.");let i=await this.#r(),r=Object.freeze({...e.assignment.semanticIdentity,semanticBoundaryId:e.semanticBoundaryId}),o=(t=e.assignment.exposureToken,n=r,`exposure:${t}:${p(n)}`),d=await this.#i.find(o);if(d)return a(d);let u=await this.#i.snapshot(e.sessionId,r.runId),m="exploratory"===e.assignment.evidencePosture.evidenceStrength,I=s({identity:i,identityTransitionAfterAssignment:i.generation!==e.assignment.identityGeneration||i.pseudonymousUnitToken!==e.assignment.semanticIdentity.assignmentUnitToken,ingestionVerified:!m,measurement:e.measurement,proofVerified:!m,queueSnapshot:u,thresholds:this.#s}),y=c(e.assignment.evidencePosture,I),f=await this.#i.reserveSequence(e.sessionId,r.runId),v=h({attempts:0,createdAt:e.occurredAt,evidencePosture:y,expiresAt:E(e.occurredAt,this.#e.recordTtlMilliseconds),idempotencyKey:o,identityGeneration:i.generation,kind:"exposure",payload:{armId:e.assignment.armId,exposureBoundary:e.semanticBoundaryId,exposureClaim:"explicit-application-boundary",exposureToken:e.assignment.exposureToken,humanExposureClaim:!1,renderExposureClaim:!1,semanticIdentity:r},pseudonymousUnitToken:e.assignment.semanticIdentity.assignmentUnitToken,runId:r.runId,sequence:f,sessionId:e.sessionId});return await this.#i.append(v,this.#o()),a(v)}async observe(e){b(e.occurredAt,"occurredAt"),T(e.sessionId,"sessionId"),T(e.metricRevisionId,"metricRevisionId"),T(e.observationId,"observationId"),T(e.valueProvenance.sourceRevision,"sourceRevision"),T(e.valueProvenance.valueField,"valueField"),l(e.measurement),function(e){if("boolean"!=typeof e&&!Number.isFinite(e))throw w("CONFIG_EXPERIMENT_OBSERVATION_VALUE_INVALID","Metric observation values must be boolean or finite numbers.")}(e.value);let t=function(e,t){if(t.length>32||new Set(t).size!==t.length)throw w("CONFIG_EXPERIMENT_DIMENSION_ALLOWLIST_INVALID","Browser metric dimension allowlists must be unique and contain at most 32 keys.");for(let e of t)T(e,"allowedDimensionKey");let n=new Set(t);return Object.freeze(Object.fromEntries(Object.entries(e).filter(([e])=>n.has(e)).sort(([e],[t])=>e.localeCompare(t))))}(e.dimensionValues,e.allowedDimensionKeys);if(e.assignment.record.sessionId!==e.sessionId)throw w("CONFIG_EXPERIMENT_OBSERVATION_SESSION_MISMATCH","Metric observations must remain in the durable assignment session.");let n=await this.#i.find(e.assignment.record.idempotencyKey);if(n?.kind!=="assignment")throw w("CONFIG_EXPERIMENT_OBSERVATION_ASSIGNMENT_REQUIRED","Metric observations require a previously persisted assignment receipt.");var i,r=n,o=e.assignment;if(r.sessionId!==o.record.sessionId||r.runId!==o.semanticIdentity.runId||r.pseudonymousUnitToken!==o.semanticIdentity.assignmentUnitToken||R(r.payload)!==R(o.record.payload))throw w("CONFIG_EXPERIMENT_OBSERVATION_ASSIGNMENT_CONFLICT","Metric observation assignment receipts must match the persisted assignment identity and content.");let d=await this.#r(),a=Object.freeze({metricRevisionId:e.metricRevisionId,sourceRevision:e.valueProvenance.sourceRevision,valueField:e.valueProvenance.valueField}),m=["observation",(i={analysisUnitToken:e.assignment.semanticIdentity.assignmentUnitToken,metricRevisionId:e.metricRevisionId,observationId:e.observationId,runId:e.assignment.semanticIdentity.runId}).runId,i.metricRevisionId,i.analysisUnitToken,i.observationId].join(":"),I=await this.#i.find(m);if(I)return function(e,t){let n=e.payload;if("observation"!==e.kind||!n.event||n.event.metricRevisionId!==t.metricRevisionId||n.event.observationId!==t.observationId||!Object.is(n.event.value,t.value)||R(n.event.dimensionValues)!==R(t.dimensions)||R(n.valueProvenance)!==R(t.valueProvenance))throw w("CONFIG_EXPERIMENT_IDEMPOTENCY_CONFLICT","A semantic metric observation retry changed its evidence content.")}(I,{dimensions:t,metricRevisionId:e.metricRevisionId,observationId:e.observationId,value:e.value,valueProvenance:a}),u(I);let p=await this.#i.snapshot(e.sessionId,e.assignment.semanticIdentity.runId),y="exploratory"===e.assignment.evidencePosture.evidenceStrength,f=s({identity:d,identityTransitionAfterAssignment:d.generation!==e.assignment.identityGeneration||d.pseudonymousUnitToken!==e.assignment.semanticIdentity.assignmentUnitToken,ingestionVerified:!y,measurement:e.measurement,proofVerified:!y,queueSnapshot:p,thresholds:this.#s}),v=c(e.assignment.evidencePosture,f),g=await this.#i.reserveSequence(e.sessionId,e.assignment.semanticIdentity.runId),_=Object.freeze({eventId:`${e.sessionId}-${e.assignment.semanticIdentity.runId}-${g}`,producerEpoch:`identity-${d.generation}`,producerId:"browser-config-experiment",producerSequence:String(g),producerSessionId:e.sessionId}),N=e.createObservationEvent({analysisUnitToken:e.assignment.semanticIdentity.assignmentUnitToken,assignment:e.assignment,dimensionValues:t,metricRevisionId:e.metricRevisionId,observationId:e.observationId,occurredAt:e.occurredAt,transport:_,value:e.value,valueProvenance:a});var O=N,k={analysisUnitToken:e.assignment.semanticIdentity.assignmentUnitToken,dimensions:t,metricRevisionId:e.metricRevisionId,observationId:e.observationId,occurredAt:e.occurredAt,runId:e.assignment.semanticIdentity.runId,transport:_,value:e.value};if("runbound.config.experiment-observation.v1"!==O.formatVersion||O.analysisUnitToken!==k.analysisUnitToken||O.metricRevisionId!==k.metricRevisionId||O.observationId!==k.observationId||O.occurredAt!==k.occurredAt||O.runId!==k.runId||O.eventId!==k.transport.eventId||O.producerEpoch!==k.transport.producerEpoch||O.producerId!==k.transport.producerId||O.producerSequence!==k.transport.producerSequence||O.producerSessionId!==k.transport.producerSessionId||!Object.is(O.value,k.value)||R(O.dimensionValues)!==R(k.dimensions))throw w("CONFIG_EXPERIMENT_OBSERVATION_IDENTITY_MISMATCH","Browser observation factories cannot change reserved identity, redacted dimensions, value, or transport.");for(let[e,t]of Object.entries({appId:O.appId,configId:O.configId,projectId:O.projectId,tenantId:O.tenantId}))T(t,e);b(O.createdAt,"event.createdAt"),b(O.receivedAt,"event.receivedAt");let x=h({attempts:0,createdAt:e.occurredAt,evidencePosture:v,expiresAt:E(e.occurredAt,this.#e.recordTtlMilliseconds),idempotencyKey:m,identityGeneration:d.generation,kind:"observation",payload:{event:N,valueProvenance:a},pseudonymousUnitToken:e.assignment.semanticIdentity.assignmentUnitToken,runId:e.assignment.semanticIdentity.runId,sequence:g,sessionId:e.sessionId});return await this.#i.append(x,this.#o()),u(x)}async flush(e){let t;b(e.now,"now"),T(e.runId,"runId"),T(e.sessionId,"sessionId");let i=e.background?this.#e.maximumBackgroundFlushEvents:this.#e.maximumForegroundFlushEvents;if(e.offline){let t=await this.#i.snapshot(e.sessionId,e.runId);return g(0,0,e.background,!0,t.pendingRecords,!1)}let s=await this.#i.pending({limit:i,now:e.now,runId:e.runId,sessionId:e.sessionId});if(0===s.length)return g(0,0,e.background,!1,0,!1);let r=!1;try{var o,d;t=await (e.background?(o=e.deliver(Object.freeze({records:s,runId:e.runId,sessionId:e.sessionId})),d=this.#e.maximumBackgroundFlushMilliseconds,new Promise((e,t)=>{let n=globalThis.setTimeout(()=>{t(w("CONFIG_EXPERIMENT_BACKGROUND_FLUSH_TIMEOUT","Background experiment delivery exceeded its frozen time bound."))},d);o.then(t=>{globalThis.clearTimeout(n),e(t)},e=>{globalThis.clearTimeout(n),t(e)})})):e.deliver(Object.freeze({records:s,runId:e.runId,sessionId:e.sessionId})))}catch(i){r=i instanceof n&&"CONFIG_EXPERIMENT_BACKGROUND_FLUSH_TIMEOUT"===i.code,await this.#i.failDelivery(s.map(e=>e.idempotencyKey),this.#e.maximumDeliveryAttempts);let t=await this.#i.snapshot(e.sessionId,e.runId);return g(0,s.length,e.background,!1,t.pendingRecords,r)}let a=new Set(s.map(e=>e.idempotencyKey)),u=[...new Set(t.acceptedIdempotencyKeys)];if(u.some(e=>!a.has(e)))throw w("CONFIG_EXPERIMENT_DELIVERY_RECEIPT_INVALID","Delivery receipts may acknowledge only records in the submitted batch.");let c=new Set(u),m=s.map(e=>e.idempotencyKey).filter(e=>!c.has(e));await this.#i.acknowledge(u),await this.#i.failDelivery(m,this.#e.maximumDeliveryAttempts);let l=await this.#i.snapshot(e.sessionId,e.runId);return g(u.length,s.length,e.background,!1,l.pendingRecords,!1)}async #r(){let e=await this.#t.load();if(e?.state!=="active")throw w("CONFIG_EXPERIMENT_IDENTITY_UNAVAILABLE","An active consented first-party pseudonymous identity is required.");return e}#o(){return{maximumPendingRecords:this.#e.maximumPendingRecords,maximumRecordBytes:this.#e.maximumRecordBytes}}}function s(e){l(e.measurement),I(e.thresholds);let t=[],n=[];e.identity.firstParty&&"granted"===e.identity.consent||t.push("first-party-consent-unavailable"),e.proofVerified||t.push("assignment-proof-unverified"),e.ingestionVerified||t.push("trusted-ingestion-receipt-unverified"),(!Number.isFinite(e.measurement.clockSkewMilliseconds)||Math.abs(e.measurement.clockSkewMilliseconds)>e.thresholds.maximumClockSkewMilliseconds)&&n.push("clock-skew-threshold-failed"),e.thresholds.requireHumanTraffic&&"human"!==e.measurement.automatedTraffic&&n.push("automated-traffic-threshold-failed"),e.thresholds.requireUnblockedTransport&&(e.measurement.adBlockerDetected||e.measurement.transportBlocked)&&n.push("transport-blocking-threshold-failed"),(e.identity.transitionCount>e.thresholds.maximumIdentityTransitionsPerRun||e.identityTransitionAfterAssignment)&&n.push("identity-transition-threshold-failed");let i=e.measurement.observedRecords,s=e.measurement.missingRecords;s/Math.max(1,i+s)>e.thresholds.maximumMissingnessRate&&n.push("missingness-threshold-failed"),e.queueSnapshot.lostRecords/Math.max(1,e.queueSnapshot.lostRecords+e.queueSnapshot.pendingRecords+i)>e.thresholds.maximumLossRate&&n.push("queue-loss-threshold-failed");let r=Object.freeze([...t,...n].sort());return Object.freeze({authorizationCapabilities:Object.freeze([]),authorizationEligible:!1,blockingReasons:r,evidencePurpose:"causalEvidenceOnly",evidenceStrength:t.length>0?"exploratory":n.length>0?"advisory":"confirmatory",frozenThresholdRevision:e.thresholds.revision,humanExposureClaim:!1,renderExposureClaim:!1})}class r{#d;async load(){return this.#d?structuredClone(this.#d):void 0}async save(e){this.#d=structuredClone(e)}}class o{#a=new Map;#u=new Map;#c=new Map;#m=new Map;#l=new Map;async acknowledge(e){for(let t of e)this.#I(t)}async append(e,t){var n,i=t;if(!Number.isSafeInteger(i.maximumPendingRecords)||i.maximumPendingRecords<1||!Number.isSafeInteger(i.maximumRecordBytes)||i.maximumRecordBytes<1)throw w("CONFIG_EXPERIMENT_QUEUE_LIMIT_INVALID","Durable queue limits must be positive safe integers.");let s=this.#u.get(e.idempotencyKey);if(s){if(R(s.payload)!==R(e.payload))throw w("CONFIG_EXPERIMENT_IDEMPOTENCY_CONFLICT","An experiment idempotency key cannot bind different content.");return"duplicate"}if(n=e,new TextEncoder().encode(R(n)).byteLength>t.maximumRecordBytes)throw this.#p(e),w("CONFIG_EXPERIMENT_RECORD_BOUND_EXCEEDED","Browser experiment record exceeds its frozen byte bound.");for(;this.#l.size>=t.maximumPendingRecords;){let e=y(this.#l.values())[0];if(!e)break;this.#h(e.idempotencyKey),this.#p(e)}let r=h({...e,attempts:0});return this.#l.set(f(r),r),this.#u.set(r.idempotencyKey,r),this.#a.set(r.idempotencyKey,0),"stored"}async failDelivery(e,t){for(let n of e){if(!this.#u.has(n))continue;let e=(this.#a.get(n)??0)+1;if(this.#a.set(n,e),e>=t){let e=this.#u.get(n);this.#h(n),e&&this.#p(e)}else{let t=this.#u.get(n);if(t){let i=h({...t,attempts:e});this.#l.set(f(i),i),this.#u.set(n,i)}}}}async find(e){let t=this.#u.get(e);return t?structuredClone(t):void 0}async pending(e){let t=Date.parse(e.now);for(let e of this.#l.values())Date.parse(e.expiresAt)<=t&&(this.#h(e.idempotencyKey),this.#p(e));return y(this.#l.values()).filter(t=>t.sessionId===e.sessionId&&t.runId===e.runId).slice(0,e.limit).map(e=>structuredClone(e))}async purgeIdentity(e){let t=[...this.#u.values()].filter(t=>t.pseudonymousUnitToken===e);for(let e of t)this.#h(e.idempotencyKey);return t.length}async reserveSequence(e,t){let n=`${e}\u0000${t}`,i=(this.#m.get(n)??0)+1;return this.#m.set(n,i),i}async snapshot(e,t){let n=v(e,t),i=[...this.#l.values()].filter(n=>n.sessionId===e&&n.runId===t).length;return Object.freeze({lostRecords:this.#c.get(n)??0,pendingRecords:i})}#p(e){let t=v(e.sessionId,e.runId);this.#c.set(t,(this.#c.get(t)??0)+1)}#I(e){let t=this.#u.get(e);t&&(this.#l.delete(f(t)),this.#a.delete(e))}#h(e){let t=this.#u.get(e);t&&(this.#l.delete(f(t)),this.#u.delete(e),this.#a.delete(e))}}function d(e,t){return Object.freeze({armId:t.armId,evidencePosture:e.evidencePosture,exposureToken:t.exposureToken,identityGeneration:e.identityGeneration,record:e,semanticIdentity:t.semanticIdentity,status:"assigned"})}function a(e){return Object.freeze({evidencePosture:e.evidencePosture,exposureRecorded:!0,humanExposureClaim:!1,record:e,renderExposureClaim:!1,status:"exposed"})}function u(e){let t=e.payload;return Object.freeze({authorizationCapabilities:Object.freeze([]),authorizationEligible:!1,evidencePosture:e.evidencePosture,event:structuredClone(t.event),record:e,status:"observed",valueProvenance:structuredClone(t.valueProvenance)})}function c(e,t){let n={advisory:1,confirmatory:2,exploratory:0},i=n[e.evidenceStrength]<=n[t.evidenceStrength]?e.evidenceStrength:t.evidenceStrength;return Object.freeze({...t,blockingReasons:Object.freeze([...new Set([...e.blockingReasons,...t.blockingReasons])].sort()),evidenceStrength:i})}async function m(e,t,n){return"serverSigned"===t.kind?e.verifyServerAssignment(t,n):e.verifyLocalArtifact(t,n)}function l(e){if(!Number.isFinite(e.clockSkewMilliseconds)||!Number.isSafeInteger(e.missingRecords)||e.missingRecords<0||!Number.isSafeInteger(e.observedRecords)||e.observedRecords<0)throw w("CONFIG_EXPERIMENT_MEASUREMENT_INVALID","Experiment measurement counters and clock skew must be finite and non-negative where applicable.")}function I(e){if(T(e.revision,"threshold revision"),!Number.isFinite(e.maximumClockSkewMilliseconds)||e.maximumClockSkewMilliseconds<0||!Number.isSafeInteger(e.maximumIdentityTransitionsPerRun)||e.maximumIdentityTransitionsPerRun<0||!N(e.maximumLossRate)||!N(e.maximumMissingnessRate)||!0!==e.requireHumanTraffic||!0!==e.requireUnblockedTransport)throw w("CONFIG_EXPERIMENT_THRESHOLDS_INVALID","Confirmatory browser thresholds must be finite, bounded, and fail closed.")}function p(e){return[e.runId,e.iterationId,e.assignmentEpoch,e.assignmentUnitToken,e.semanticBoundaryId,e.switchbackPeriodId??""].join(":")}function h(e){return Object.freeze({...structuredClone(e),evidencePosture:Object.freeze({...e.evidencePosture,authorizationCapabilities:Object.freeze([]),blockingReasons:Object.freeze([...e.evidencePosture.blockingReasons])})})}function y(e){return[...e].sort((e,t)=>e.sessionId.localeCompare(t.sessionId)||e.runId.localeCompare(t.runId)||e.sequence-t.sequence||e.idempotencyKey.localeCompare(t.idempotencyKey))}function f(e){return`${e.sessionId}\u0000${e.runId}\u0000${e.sequence}`}function v(e,t){return`${e}\u0000${t}`}function g(e,t,n,i,s,r){return Object.freeze({acknowledged:e,attempted:t,background:n,offline:i,pending:s,timedOut:r})}function E(e,t){return new Date(Date.parse(e)+t).toISOString()}function b(e,t){if("string"!=typeof e||!Number.isFinite(Date.parse(e)))throw w("CONFIG_EXPERIMENT_TIMESTAMP_INVALID",`${t} must be an ISO-compatible timestamp.`)}function T(e,t){if(!_(e)||e.length>512)throw w("CONFIG_EXPERIMENT_IDENTIFIER_INVALID",`${t} must be a bounded non-empty identifier.`)}function _(e){return"string"==typeof e&&e.trim().length>0}function N(e){return Number.isFinite(e)&&e>=0&&e<=1}function R(e){return Array.isArray(e)?`[${e.map(R).join(",")}]`:e&&"object"==typeof e?`{${Object.entries(e).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${JSON.stringify(e)}:${R(t)}`).join(",")}}`:JSON.stringify(e)}function w(e,t){return new n(e,t)}e.d(t,{KA:()=>i,SW:()=>r,eL:()=>n,nS:()=>o,nq:()=>s});var O=t.eL,k=t.KA,x=t.nS,A=t.SW,S=t.nq;export{O as BrowserExperimentRuntimeError,k as ConfigExperimentBrowserRuntime,x as MemoryBrowserExperimentDurableQueue,A as MemoryBrowserExperimentIdentityStore,S as assessBrowserExperimentEvidence};
@@ -0,0 +1,67 @@
1
+ import type { ConfigAudienceArtifact, ConfigContextContract, ConfigDecisioningActivationSet, ConfigDecisionResult, ConfigEvaluationContext } from "./internal/config-contract/decisioning.js";
2
+ export type ConfigBrowserPublicDecisionManifest = Readonly<{
3
+ activationSequence: number;
4
+ activationSetId: string;
5
+ artifactDigest: string;
6
+ audience: "localPublic";
7
+ configId: string;
8
+ contextContractDigest: string;
9
+ currentUntil: string;
10
+ formatVersion: "runbound.config.browser-public-decision-manifest.v1";
11
+ keyId: string;
12
+ notBefore: string;
13
+ projectId: string;
14
+ schemaDigest: string;
15
+ signature: string;
16
+ signatureAlgorithm: "ECDSA_SHA_256" | "Ed25519";
17
+ softwareReleaseId: string;
18
+ tenantId: string;
19
+ }>;
20
+ export type ConfigBrowserPublicDecisionBundle = Readonly<{
21
+ activationSet: ConfigDecisioningActivationSet;
22
+ artifact: ConfigAudienceArtifact;
23
+ contextContract: ConfigContextContract;
24
+ formatVersion: "runbound.config.browser-public-decision-bundle.v1";
25
+ manifest: ConfigBrowserPublicDecisionManifest;
26
+ }>;
27
+ export type ConfigBrowserPublicDecisionScope = Readonly<{
28
+ activationSetId: string;
29
+ configId: string;
30
+ keyId: string;
31
+ minimumActivationSequenceExclusive?: number;
32
+ projectId: string;
33
+ schemaDigest: string;
34
+ softwareReleaseId: string;
35
+ tenantId: string;
36
+ }>;
37
+ export type ConfigBrowserPublicDecisionTrustedKey = Readonly<{
38
+ algorithm: "ECDSA_SHA_256" | "Ed25519";
39
+ key: CryptoKey;
40
+ keyId: string;
41
+ }>;
42
+ export type ConfigBrowserPublicDecisionClientOptions = Readonly<{
43
+ bundle: ConfigBrowserPublicDecisionBundle | Uint8Array | string;
44
+ crypto?: Crypto;
45
+ now?: Date;
46
+ scope: ConfigBrowserPublicDecisionScope;
47
+ trustedKeys: readonly ConfigBrowserPublicDecisionTrustedKey[];
48
+ }>;
49
+ export declare class ConfigBrowserPublicDecisionError extends Error {
50
+ readonly code: string;
51
+ constructor(code: string, message: string);
52
+ }
53
+ export declare class ConfigBrowserPublicDecisionClient {
54
+ #private;
55
+ private constructor();
56
+ static create(options: ConfigBrowserPublicDecisionClientOptions): Promise<ConfigBrowserPublicDecisionClient>;
57
+ evaluate(decisionId: string, context: ConfigEvaluationContext, now?: Date): ConfigDecisionResult;
58
+ status(): Readonly<{
59
+ activationSequence: number;
60
+ activationSetId: string;
61
+ artifactDigest: string;
62
+ audience: "localPublic";
63
+ currentUntil: string;
64
+ schemaDigest: string;
65
+ softwareReleaseId: string;
66
+ }>;
67
+ }