stillmade-sdk 0.1.4 → 0.1.6

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.
Files changed (101) hide show
  1. package/.claude/settings.json +15 -0
  2. package/.cursor/hooks.json +11 -0
  3. package/.gitignore +2 -0
  4. package/.stillmade/sdk-baseline.json +65 -0
  5. package/AGENTS.md +29 -7
  6. package/AGENT_CONTROL.md +2 -1
  7. package/BROWSER_APPLICATIONS.md +128 -3
  8. package/GITHUB_APPLICATIONS.md +184 -1
  9. package/NATIVE_APPLICATION_ADAPTATION.md +565 -0
  10. package/PASTE_TO_LLM.md +29 -10
  11. package/PORTABLE.md +5 -4
  12. package/REMIX.md +16 -10
  13. package/SDK.md +511 -46
  14. package/UI_AESTHETIC.md +37 -6
  15. package/api-index.json +90 -0
  16. package/blocks/connected-app/versions/{1.2.0 → 1.5.0}/task-selection.js +5 -3
  17. package/blocks/connected-app/versions/{1.4.0 → 1.5.0}/view.js +4 -2
  18. package/blocks/native/sm.ai-decide/versions/1.0.0/package.js +149 -0
  19. package/blocks/native/sm.ai-write/versions/1.0.0/package.js +147 -0
  20. package/blocks/native/sm.connect-app/versions/1.0.0/package.js +55 -0
  21. package/blocks/native/sm.contact-fields/versions/1.0.0/package.js +197 -0
  22. package/blocks/native/sm.continue-if/versions/1.0.0/package.js +168 -0
  23. package/blocks/native/sm.generate-image/versions/2.1.0/package.js +101 -0
  24. package/blocks/native/sm.meta-creative/versions/1.0.0/package.js +206 -0
  25. package/blocks/native/sm.new-items/versions/1.0.0/package.js +191 -0
  26. package/blocks/native/sm.voiceover/versions/2.1.5/package.js +179 -0
  27. package/blocks/native/sm.voiceover/versions/2.1.6/package.js +179 -0
  28. package/blocks/native/sm.when/versions/1.0.0/package.js +129 -0
  29. package/blocks/native/sm.write-summary/versions/1.0.0/package.js +133 -0
  30. package/blocks/occasion-story/package.js +1 -1
  31. package/blocks/occasion-story/versions/1.0.2/package.js +1682 -0
  32. package/blocks/photo-reel-composer/versions/1.0.1/package.js +3026 -0
  33. package/blocks/release-visuals/package.js +1 -1
  34. package/blocks/release-visuals/versions/1.0.2/package.js +1500 -0
  35. package/blocks/short-form-finish/package.js +1 -1
  36. package/blocks/short-form-finish/versions/1.0.3/package.js +1026 -0
  37. package/package.json +1 -1
  38. package/packages/block-cli/cli.js +16 -1
  39. package/packages/block-cli/format.js +2 -0
  40. package/packages/block-cli/ready-gate.js +164 -0
  41. package/packages/block-platform/block-readiness.js +3 -2
  42. package/packages/block-platform/canvas.js +1 -1
  43. package/packages/block-platform/catalog.js +6 -4
  44. package/packages/block-platform/chat-requirements.js +8 -3
  45. package/packages/block-platform/collaboration.js +5 -5
  46. package/packages/block-platform/first-revenue-project-types.js +2 -2
  47. package/packages/block-platform/flow-inspection.js +2 -2
  48. package/packages/block-platform/native-application-validation-context.js +20 -0
  49. package/packages/block-platform/plain-descriptions.js +67 -0
  50. package/packages/block-platform/product-ad-variants-project-type.js +6 -3
  51. package/packages/block-platform/project-context.js +4 -4
  52. package/packages/block-platform/project-type-composer.js +21 -12
  53. package/packages/block-platform/project-type-draft.js +2 -2
  54. package/packages/block-platform/project-type-recommendations.js +1 -1
  55. package/packages/block-platform/project-types.js +9 -4
  56. package/packages/block-platform/revenue-review-project-types.js +6 -5
  57. package/packages/block-platform/segment-runner.js +35 -11
  58. package/packages/block-platform/step-groups.js +78 -0
  59. package/packages/block-platform/steps.js +12 -8
  60. package/packages/block-platform/type-workspace.js +4 -4
  61. package/packages/block-platform/workflow-loop.js +4 -0
  62. package/packages/block-platform/workspace-input-readset.js +18 -4
  63. package/packages/block-sdk/admission.js +5 -17
  64. package/packages/block-sdk/agent-inputs.js +2 -0
  65. package/packages/block-sdk/agent-inspection.js +4 -2
  66. package/packages/block-sdk/browser-application-artifacts.js +1 -1
  67. package/packages/block-sdk/browser-application-execution.d.ts +2 -2
  68. package/packages/block-sdk/browser-application-execution.js +30 -12
  69. package/packages/block-sdk/browser-application-guest.d.ts +2 -0
  70. package/packages/block-sdk/browser-application-guest.js +16 -9
  71. package/packages/block-sdk/browser-application.d.ts +12 -4
  72. package/packages/block-sdk/browser-application.js +27 -13
  73. package/packages/block-sdk/builder-contract.js +25 -17
  74. package/packages/block-sdk/capabilities.js +38 -19
  75. package/packages/block-sdk/connected-app.js +26 -6
  76. package/packages/block-sdk/connection-adapters.js +19 -2
  77. package/packages/block-sdk/contracts-core.js +8 -0
  78. package/packages/block-sdk/contracts.js +9 -5
  79. package/packages/block-sdk/document-drafts.js +7 -1
  80. package/packages/block-sdk/document-store-binding.js +2 -1
  81. package/packages/block-sdk/file-bundle.js +4 -1
  82. package/packages/block-sdk/first-party-packages.js +18 -2
  83. package/packages/block-sdk/generation-catalog.js +3 -2
  84. package/packages/block-sdk/index.d.ts +20 -1
  85. package/packages/block-sdk/index.js +1 -0
  86. package/packages/block-sdk/native-application-package.d.ts +22 -0
  87. package/packages/block-sdk/native-application-package.js +77 -0
  88. package/packages/block-sdk/native_adapter.py +76 -0
  89. package/packages/block-sdk/package.json +1 -0
  90. package/packages/block-sdk/pipeline-runtime-host.d.ts +2 -0
  91. package/packages/block-sdk/project-flow-contract.js +2 -0
  92. package/packages/block-sdk/remix-policy.js +3 -1
  93. package/packages/block-sdk/resolver-admission.js +17 -0
  94. package/packages/block-sdk/runtime.js +4 -1
  95. package/packages/block-sdk/schema-fields.js +34 -2
  96. package/packages/block-sdk/semantic-relationships.js +14 -0
  97. package/packages/block-sdk/shared-controls.js +3 -1
  98. package/packages/block-sdk/shared-view.js +6 -1
  99. package/packages/block-sdk/source-rights.js +6 -5
  100. package/packages/block-sdk/testing.js +3 -0
  101. package/packages/design-system/theme.css +12 -3
@@ -42,12 +42,24 @@ export async function captureWorkspaceInputReadSet({manifest,input,project,scope
42
42
  documents.set(nativeScope,document);ports.push({key,context:port.context,scope:nativeScope,inputDigest:await digest(input[key])});}
43
43
  }
44
44
  assert(Array.isArray(consumedOutputs)&&consumedOutputs.length<=128,'CONFLICT','Too many consumed output receipts.');
45
- const collectionOutputs=collections.flatMap(row=>row.consumedOutputs);
45
+ const browserMaterials=[],collectionOutputs=collections.flatMap(row=>row.consumedOutputs);
46
46
  assert(collectionOutputs.length+consumedOutputs.length<=128,'CONFLICT','Too many consumed output receipts.');
47
47
  for(const consumed of [...consumedOutputs,...collectionOutputs]){
48
48
  const {key,record,port,placementId,value,consumedValue}=consumed;
49
49
  // Context resolution, user answers and optional branches can replace a wire.
50
50
  if((manifest.inputs?.[key]?.context&&!collectionOutputs.includes(consumed))||input[key]===undefined||stableStringify(input[key])!==stableStringify(consumedValue))continue;
51
+ // This is a source selector, never an execution attestation. Acceptance
52
+ // independently checks the completed project ledger under its transaction.
53
+ if(record?.applicationReceipt){
54
+ const node=project.canvas?.nodes?.find(node=>node.id===placementId),data=node?.data,receipt=record.applicationReceipt;
55
+ const matches=data?.blockPackage?.manifest?.runtime==='browser-application'&&data.blockStale!==true&&bounded(port)&&bounded(record.runId)&&
56
+ receipt.phase==='completed'&&receipt.packageDigest===data.blockDigest&&receipt.inputDigest===record.inputDigest&&receipt.outputDigest===await digest(data.blockOutputs??{})&&
57
+ (record.packageDigest??record.digest)===data.blockDigest&&stableStringify(data.blockLastRun?.applicationReceipt??null)===stableStringify(receipt)&&
58
+ data.blockLastRun?.runId===record.runId&&stableStringify(data.blockLastRun?.applicationCheckpoint??null)===stableStringify(record.applicationCheckpoint??null)&&
59
+ stableStringify(data.blockApplicationDraft??null)===stableStringify(record.applicationCheckpoint?.draft??null)&&stableStringify(data.blockOutputs?.[port]??null)===stableStringify(value);
60
+ if(matches){browserMaterials.push({key,value:input[key],readSet:createWorkspaceReadSet([await workspaceSourceRead('canvas',project.canvas,['nodes',{id:placementId},'data'])]),documents:{canvas:project.canvas}});continue;}
61
+ unprotected.push({key,context:'upstream',reason:'missing-original-output-receipt'});continue;
62
+ }
51
63
  const original=record?.workspaceInputReads;
52
64
  if(original?.version!==2||!original.outputDigests||hasUnprovenStateOutput(record)){unprotected.push({key,context:'upstream',reason:'missing-original-output-receipt'});continue;}
53
65
  validateWorkspaceInputReadSet(original,{runId:record.runId,packageDigest:record.packageDigest??record.digest,inputDigest:record.inputDigest});
@@ -58,15 +70,16 @@ export async function captureWorkspaceInputReadSet({manifest,input,project,scope
58
70
  const reads=[];for(const [nativeScope,document] of documents)reads.push(await workspaceSourceRead(nativeScope,document,[]));
59
71
  const directReadSet=reads.length?createWorkspaceReadSet(reads):null;
60
72
  for(const collection of collections)for(const [scope,document] of Object.entries(collection.documents)){if(documents.has(scope))assert(stableStringify(documents.get(scope))===stableStringify(document),'CONFLICT','Original collection and context documents disagree.');else documents.set(scope,structuredClone(document));}
61
- const materialReads=[];assert(Array.isArray(materialSources)&&materialSources.length<=128,'CONFLICT','Too many material input sources.');
62
- for(const material of materialSources){const {key,value,readSet,documents:originalDocuments}=material;assert(bounded(key)&&Object.hasOwn(manifest.inputs,key)&&input[key]!==undefined&&stableStringify(input[key])===stableStringify(value)&&plain(originalDocuments),'CONFLICT','The material source no longer matches its consumed input.');validateWorkspaceReadSet(readSet);await assertWorkspaceReadSet(new Map(Object.entries(originalDocuments)),readSet);
73
+ const materialReads=[];assert(Array.isArray(materialSources)&&materialSources.length+browserMaterials.length<=128,'CONFLICT','Too many material input sources.');
74
+ for(const material of [...materialSources,...browserMaterials]){const {key,value,readSet,documents:originalDocuments}=material;assert(bounded(key)&&Object.hasOwn(manifest.inputs,key)&&input[key]!==undefined&&stableStringify(input[key])===stableStringify(value)&&plain(originalDocuments),'CONFLICT','The material source no longer matches its consumed input.');validateWorkspaceReadSet(readSet);await assertWorkspaceReadSet(new Map(Object.entries(originalDocuments)),readSet);
63
75
  const usedScopes=new Set(readSet.reads.map(read=>read.scope));assert(Object.keys(originalDocuments).every(nativeScope=>usedScopes.has(nativeScope)),'CONFLICT','Material documents include an unused source.');
64
76
  for(const [nativeScope,document] of Object.entries(originalDocuments)){if(documents.has(nativeScope))assert(stableStringify(documents.get(nativeScope))===stableStringify(document),'CONFLICT','Original material and context documents disagree.');else documents.set(nativeScope,structuredClone(document));}
65
77
  materialReads.push({key,inputDigest:await digest(input[key]),readSet:structuredClone(readSet)});
66
78
  }
67
79
  const memory=await captureWorkspaceStateSource({manifest,state,source:stateSource,packageDigest});if(memory)unprotected.push(...memory.unprotected);else if(manifest.state)unprotected.push({key:'memory',context:'state',reason:'missing-original-state-receipt'});
68
80
  const collectionReads=collections.map(({documents:ignored,consumedOutputs:ignoredOutputs,...row})=>row);
69
- const evidence={version:2,runId,packageDigest,inputDigest,...(memory?{stateSource:memory}:{}),...(collectionReads.length?{collections:collectionReads}:{}),dispatchInputDigest:await digest(input),ports,unprotected,inherited,...(materialReads.length?{materialSources:materialReads}:{}),directReadSet,readSet:unionReads([directReadSet,memory?.readSet,...inherited.map(row=>row.readSet),...materialReads.map(row=>row.readSet),...collectionReads.map(row=>row.readSet)])};
81
+ const inputDigests=Object.fromEntries(await Promise.all(Object.keys(manifest.inputs??{}).filter(key=>input[key]!==undefined).map(async key=>[key,await digest(input[key])])));
82
+ const evidence={version:2,runId,packageDigest,inputDigest,inputDigests,...(memory?{stateSource:memory}:{}),...(collectionReads.length?{collections:collectionReads}:{}),dispatchInputDigest:await digest(input),ports,unprotected,inherited,...(materialReads.length?{materialSources:materialReads}:{}),directReadSet,readSet:unionReads([directReadSet,memory?.readSet,...inherited.map(row=>row.readSet),...materialReads.map(row=>row.readSet),...collectionReads.map(row=>row.readSet)])};
70
83
  validateWorkspaceInputReadSet(evidence,{runId,packageDigest,inputDigest});return {evidence,documents:Object.fromEntries(documents)};
71
84
  }
72
85
  function validateDerivedPorts(ports){
@@ -83,6 +96,7 @@ export function validateWorkspaceInputReadSet(evidence,{runId,packageDigest,inpu
83
96
  if(direct)validateWorkspaceReadSet(direct);
84
97
  assert(scopes.size===(direct?.reads.length||0)&&new Set((direct?.reads||[]).map(read=>read.scope)).size===scopes.size&&(direct?.reads||[]).every(read=>scopes.has(read.scope)&&read.path.length===0),'CONFLICT','The native workspace source reads are incomplete.');
85
98
  if(evidence.version===2){
99
+ if(evidence.inputDigests!==undefined)assert(plain(evidence.inputDigests)&&Object.keys(evidence.inputDigests).length<=128&&Object.entries(evidence.inputDigests).every(([key,value])=>bounded(key)&&hash(value)),'CONFLICT','Dispatched input digests are invalid.');
86
100
  assert(Array.isArray(evidence.inherited)&&evidence.inherited.length<=128,'CONFLICT','Inherited source receipts are invalid.');
87
101
  for(const row of evidence.inherited){const source=row?.source;assert(plain(row)&&bounded(row.key)&&hash(row.inputDigest)&&plain(source)&&['placementId','port','runId'].every(key=>bounded(source[key]))&&['packageDigest','inputDigest','outputDigest','receiptDigest'].every(key=>hash(source[key]))&&Array.isArray(row.unprotected)&&row.unprotected.length<=128,'CONFLICT','Inherited output identity is invalid.');if(source.batchInventoryDigest!==undefined)assert(hash(source.batchInventoryDigest),'CONFLICT','Invalid inherited batch inventory identity.');if(row.readSet)validateWorkspaceReadSet(row.readSet);}
88
102
  if(evidence.stateSource){validateWorkspaceStateSource(evidence.stateSource);assert(evidence.stateSource.unprotected.every(marker=>evidence.unprotected.some(row=>stableStringify(row)===stableStringify(marker))),'CONFLICT','Memory uncertainty cannot be removed from its output evidence.');}
@@ -1,3 +1,5 @@
1
+ import {reviewResolverContract} from './resolver-admission.js';
2
+ export {reviewResolverContract} from './resolver-admission.js';
1
3
  import {checkExportPolicy} from './export-admission.js';
2
4
  import {blockRemixPolicy} from './remix-policy.js';
3
5
  import {initialBlockState,validateBlockState} from './state.js';
@@ -12,6 +14,7 @@ import {reviewModelPortability} from './model-portability.js';
12
14
  import {sharedAgentControls} from './shared-agent-controls.js';
13
15
  import {assert,safeDocument,digest,sdkCompatibility,HOSTED_OPERATION_NAMES} from './contracts.js';
14
16
  import {validatePackage,testPackage} from './testing.js';
17
+ import {NATIVE_APPLICATION_RUNTIME_REQUIREMENT} from './native-application-package.js';
15
18
 
16
19
  const hostOperations=HOSTED_OPERATION_NAMES;
17
20
  const credential=/\b(?:API_KEY|ACCESS_TOKEN|AUTH_TOKEN|CLIENT_SECRET|SECRET_KEY|[A-Z][A-Z0-9_]{1,}_(?:API_KEY|ACCESS_TOKEN|AUTH_TOKEN|TOKEN|SECRET|SECRET_KEY))\b/g;
@@ -70,27 +73,12 @@ function reviewAgentControlContract(pkg){
70
73
  const hosted=['capability','comfyui'].includes(manifest.runtime),desktopTools=(manifest.desktopTools||[]).map(tool=>({id:tool.id,adapterVersion:tool.adapterVersion,actions:[...tool.actions]}));
71
74
  return {...base,passed:true,manual:{schemaVersion:1,contract:'block-agent-json/1',block:{id:manifest.id,version:manifest.version,runtime:manifest.runtime,description:manifest.description},inputs:{json,media,context,fields:inputFields},outputs:{names:outputs,fields:outputFields},sharedView:{fields:sharedFields,dynamicFields:'Existing SDK-authored shared fields are discoverable at runtime and retain their current JSON shape.'},controls:{inspect:'inspect_app',setJsonInput:'set_input',setSharedControl:'set_shared_control',selectMedia:'select_media',selectMediaList:'select_media_list',execute:hosted?'review_generation':'run',receipt:'get_app_action',...(desktopTools.length?{desktopTools}: {})},execution:{reviewRequired:true,hostedIntegrationReview:hosted,desktopRequired:desktopTools.length>0}},message:`The host can derive exact chat and MCP controls from the pinned manifest: ${json.length} JSON input${json.length===1?'':'s'}, ${media.length} permission-checked media input${media.length===1?'':'s'}, ${context.length} host-supplied context input${context.length===1?'':'s'}, ${sharedFields.length} declared shared view control${sharedFields.length===1?'':'s'}, ${desktopTools.length} approved desktop tool${desktopTools.length===1?'':'s'}, and ${outputs.length} described output${outputs.length===1?'':'s'}. Runtime validation remains authoritative; package code cannot add hidden tools.`};
72
75
  }
73
- function reviewResolverContract(pkg){
74
- const base={name:'Input/output resolver contract',runtimeVerified:false,contract:'semantic-resolver/1'},manifest=pkg?.manifest||{},issues=[];
75
- for(const [name,port] of Object.entries(manifest.inputs||{})){
76
- if(typeof port?.semantic!=='string')issues.push(`${name} input needs an exact semantic meaning`);
77
- if(typeof port?.required!=='boolean')issues.push(`${name} input must declare whether it is required`);
78
- if(typeof port?.description!=='string'||!port.description.trim())issues.push(`${name} input needs a purpose/format description`);
79
- }
80
- for(const [name,port] of Object.entries(manifest.outputs||{})){
81
- if(typeof port?.semantic!=='string')issues.push(`${name} output needs an exact semantic meaning`);
82
- if(typeof port?.description!=='string'||!port.description.trim())issues.push(`${name} output needs a result description`);
83
- if(['json','object'].includes(port?.type?.replace(/\[\]$/,''))){if(port.schema==null)issues.push(`${name} output needs a declared field schema for safe resolution`);else if(JSON.stringify(port.schema).length>4000)issues.push(`${name} output schema exceeds the resolver manual limit`);}
84
- }
85
- if(issues.length)return {...base,passed:false,code:'RESOLVER_CONTRACT',issues:issues.slice(0,20),message:`Import is blocked until every input and output can participate in typed resolution: ${issues.slice(0,8).join('; ')}${issues.length>8?`; ${issues.length-8} more`:''}. Physical JSON types alone do not establish compatible meaning.`};
86
- const inputs=Object.entries(manifest.inputs||{}).map(([name,port])=>({name,type:port.type,semantic:port.semantic,required:port.required,sources:port.sources||[]}));
87
- const outputs=Object.entries(manifest.outputs||{}).map(([name,port])=>({name,type:port.type,semantic:port.semantic}));
88
- return {...base,passed:true,manual:{inputs,outputs},message:`All ${inputs.length} input${inputs.length===1?'':'s'} and ${outputs.length} output${outputs.length===1?'':'s'} declare physical types, semantic meanings and descriptions. Workflow composition must still validate each exact output-to-input pair, schema, adapter pin and required permission.`};
89
- }
76
+
90
77
 
91
78
  // Conservative admission checks supplement (never replace) the isolated runtime.
92
79
  // Parse source as data; no module loader, package manager, eval or shell is used.
93
80
  export function scanPackageSource(pkg) {
81
+ assert(pkg?.manifest?.runtime!=='native-application','NATIVE_APPLICATION_RUNTIME_UNAVAILABLE',NATIVE_APPLICATION_RUNTIME_REQUIREMENT);
94
82
  const exports=checkExportPolicy(pkg);
95
83
  if(!exports.passed)return {passed:false,results:[exports]};
96
84
  validatePackage(pkg);
@@ -21,6 +21,8 @@ export function parseAgentInput(manifest,args,{locked=[],busy=false,disabled=fal
21
21
  const port=manifest.inputs?.[args.field];
22
22
  if(busy||disabled||!port||port.context||locked.includes(args.field))throw new Error('This input is locked or the Block is busy.');
23
23
  if(!editable.has(port.type))throw new Error('Use the matching media or project-item control for this input.');
24
+ // A script or brief takes plain text too, stored the way its text control stores it.
25
+ if(['script','brief'].includes(port.type)&&typeof args.value==='string')args={...args,value:{schemaVersion:1,text:args.value}};
24
26
  safeDocument(args.value);
25
27
  if(JSON.stringify(args.value).length>20000||Array.isArray(args.value)&&args.value.length>100||!valueMatches(args.value,port))throw new Error('Use a compatible value with at most 100 list items and 20,000 characters.');
26
28
  const control=manifest.ui?.find(row=>row.port===args.field);
@@ -26,7 +26,9 @@ export function inspectAgentValue(value, port = {type:'json'}, {mediaVerified =
26
26
  return {status:text !== undefined ? (text.trim()?'filled':'empty') : keys.length?'filled':'empty', filled:text !== undefined ? !!text.trim() : keys.length>0, fields:keys.length};
27
27
  }
28
28
 
29
- export function inspectAgentPorts(ports = {}, values = {}, {offset=0, limit=30, locked=[],canonicalOrder=false} = {}) {
29
+ // `verifiedMedia` names ports whose media StillMade itself recorded for the
30
+ // current run (a media receipt), so their availability reads as verified.
31
+ export function inspectAgentPorts(ports = {}, values = {}, {offset=0, limit=30, locked=[],canonicalOrder=false,verifiedMedia=[]} = {}) {
30
32
  const entries = canonicalOrder?agentInputEntries({inputs:plain(ports)?ports:{}}):Object.entries(plain(ports) ? ports : {});
31
33
  const start = Math.max(0,Math.trunc(offset)||0), count = Math.max(1,Math.min(50,Math.trunc(limit)||30));
32
34
  let schemaBudget=12000;
@@ -34,7 +36,7 @@ export function inspectAgentPorts(ports = {}, values = {}, {offset=0, limit=30,
34
36
  const declared = plain(port) ? port : {type:'json'};
35
37
  const hasValue = Object.hasOwn(values || {},id), hasDefault = Object.hasOwn(declared,'default');
36
38
  const value = hasValue ? values[id] : hasDefault ? declared.default : undefined;
37
- const state = inspectAgentValue(value,declared);
39
+ const state = inspectAgentValue(value,declared,{mediaVerified:verifiedMedia.includes(id)});
38
40
  const schema=declared.schema==null?null:structuredClone(declared.schema),schemaText=schema==null?'':JSON.stringify(schema);
39
41
  const includeSchema=schemaText&&schemaText.length<=Math.min(4000,schemaBudget);if(includeSchema)schemaBudget-=schemaText.length;
40
42
  return {id,type:declared.type || 'unknown',required:declared.required!==false,...(typeof declared.description==='string'&&declared.description.trim()?{description:declared.description.slice(0,240)}:{}),...(typeof declared.semantic==='string'?{semantic:declared.semantic.slice(0,80)}:{}),...(typeof declared.role==='string'?{role:declared.role.slice(0,80)}:{}),...(Array.isArray(declared.accepts)?{accepts:declared.accepts.filter(item=>typeof item==='string').slice(0,12)}:{}),...(Number.isFinite(declared.min)?{min:declared.min}:{}),...(Number.isFinite(declared.max)?{max:declared.max}:{}),...(declared.primary===true?{primary:true}:{}),...(includeSchema?{schema}:schemaText?{schemaPartial:true}:{}),source:declared.context?'project':hasValue?'provided':hasDefault?'default':'unset',locked:!!declared.context || locked.includes(id),...state};
@@ -47,7 +47,7 @@ function sourceManifest(manifest){
47
47
  check(plain(manifest)&&manifest.runtime==='browser-application','APPLICATION_SOURCE_REFERENCE','Use a browser application manifest.');
48
48
  const errors=validateManifest(manifest).errors.filter(error=>error.path!=='runtime');
49
49
  check(errors.length===0,'APPLICATION_SOURCE_REFERENCE','Invalid application manifest: '+JSON.stringify(errors));
50
- check(!manifest.id.startsWith('sm.')&&manifest.platforms?.desktop?.supported===true&&manifest.platforms?.browser?.supported===false&&manifest.platforms?.phone?.supported===false,'APPLICATION_SOURCE_REFERENCE','This reference profile requires a non-platform, desktop-only browser application.');
50
+ check(!manifest.id.startsWith('sm.')&&manifest.platforms?.desktop?.supported===true&&typeof manifest.platforms?.browser?.supported==='boolean'&&manifest.platforms?.phone?.supported===false,'APPLICATION_SOURCE_REFERENCE','This reference profile requires a non-platform browser application with explicit browser availability and no phone build.');
51
51
  check(manifest.entry===undefined||manifest.entry==='src/application.json','APPLICATION_SOURCE_REFERENCE','Use the canonical application source entry.');
52
52
  check(!manifest.desktopTools&&!manifest.permissions.desktop?.length&&!manifest.permissions.capabilities?.length&&!manifest.permissions.actions?.length,'APPLICATION_SOURCE_REFERENCE','Application references cannot add desktop, paid integration or arbitrary action access.');
53
53
  }
@@ -1,5 +1,5 @@
1
1
  import type {Manifest} from './index.js';
2
- import type {BrowserApplicationBundle,BrowserApplicationOperation,BrowserApplicationDeepReadonly,BROWSER_APPLICATION_PROFILE} from './browser-application.js';
2
+ import type {BrowserApplicationBundle,BrowserApplicationOperation,BrowserApplicationDeepReadonly} from './browser-application.js';
3
3
 
4
4
  /** Deliberately separate from the installable Manifest/BlockPackage unions. */
5
5
  export type BrowserApplicationManifest=Omit<Manifest,'runtime'|'state'|'jobs'|'desktopTools'|'permissions'|'platforms'>&{
@@ -11,7 +11,7 @@ export interface BrowserApplicationSource {manifest:BrowserApplicationManifest;a
11
11
  export interface BrowserApplicationExecutionContext {operation:string;state:Record<string,unknown>;stateRevision:string;interactive?:boolean}
12
12
  export type BrowserApplicationMountContext=Pick<BrowserApplicationExecutionContext,'state'|'stateRevision'>;
13
13
  export interface BrowserApplicationRequest {
14
- schemaVersion:1;phase:'execute'|'mount';requestId:string;profile:typeof BROWSER_APPLICATION_PROFILE;
14
+ schemaVersion:1;phase:'execute'|'mount';requestId:string;profile:BrowserApplicationBundle['profile'];
15
15
  packageDigest:string;bundleDigest:string;inputDigest:string;stateDigest:string;stateRevision:string;
16
16
  operation:BrowserApplicationOperation|null;lifecycle:'interactive'|'headless';
17
17
  }
@@ -1,5 +1,5 @@
1
1
  import {assert,plain,safeDocument,validateManifest,validateValues,validateBlockOutputs,digest,stableStringify} from './contracts.js';
2
- import {defineBrowserApplication,BROWSER_APPLICATION_PROFILE,BROWSER_APPLICATION_LIMITS} from './browser-application.js';
2
+ import {defineBrowserApplication,BROWSER_APPLICATION_LIMITS} from './browser-application.js';
3
3
 
4
4
  const exact=(value,keys)=>plain(value)&&Object.keys(value).length===keys.length&&keys.every(key=>Object.hasOwn(value,key));
5
5
  const bounded=(value,label)=>{safeDocument(value);assert(new TextEncoder().encode(JSON.stringify(value)).length<=BROWSER_APPLICATION_LIMITS.messageBytes,'LIMIT',`${label} exceeds 4 MiB.`);};
@@ -8,9 +8,10 @@ const text=value=>typeof value==='string'&&value.trim()===value&&value.length>0&
8
8
  const freeze=value=>{if(value&&typeof value==='object'){Object.values(value).forEach(freeze);Object.freeze(value);}return value;};
9
9
  const verifiedSources=new WeakSet(),preparedRequests=new WeakSet(),sourceDigests=new WeakMap();
10
10
 
11
- /** Desktop application source contract. The ordinary local JS admission/runtime
12
- * still reject this profile; server import accepts exact operator-reviewed pins. */
13
- export async function defineBrowserApplicationSource(value){
11
+ /** Application source contract. Platform declarations do not grant execution:
12
+ * hosts still require exact reviewed pins and a trusted execution transport. */
13
+ export async function defineBrowserApplicationSource(value,{signal}={}){
14
+ signal?.throwIfAborted();
14
15
  if(verifiedSources.has(value))return value;
15
16
  safeDocument(value);check(plain(value)&&Object.keys(value).every(key=>['manifest','application','tests'].includes(key)),'Use manifest, application and optional source fixtures only.');
16
17
  check(value.manifest?.runtime==='browser-application','Use the explicit, not-yet-installed browser-application runtime.');
@@ -19,33 +20,48 @@ export async function defineBrowserApplicationSource(value){
19
20
  // is its deliberately unavailable runtime enum; never rewrite it to JS.
20
21
  const errors=validateManifest(manifest).errors.filter(error=>error.path!=='runtime');
21
22
  check(errors.length===0,JSON.stringify(errors));
23
+ check(Object.keys(manifest.outputs).length>0,'Application operations must declare at least one typed output.');
22
24
  check(!manifest.id.startsWith('sm.'),'First-party identities require the platform release registry.');
23
- check(manifest.platforms?.desktop?.supported===true&&manifest.platforms?.browser?.supported===false&&manifest.platforms?.phone?.supported===false,'This bundle profile requires an explicit desktop-only declaration.');
25
+ check(manifest.platforms?.desktop?.supported===true&&typeof manifest.platforms?.browser?.supported==='boolean'&&manifest.platforms?.phone?.supported===false,'Declare desktop and browser availability explicitly; this profile has no verified phone build.');
24
26
  check(manifest.entry===undefined||manifest.entry==='src/application.json','Use the canonical src/application.json source entry.');
25
27
  check(!manifest.desktopTools&&!manifest.permissions.desktop?.length&&!manifest.permissions.capabilities?.length&&!manifest.permissions.actions?.length,'The initial application contract grants no desktop, paid integration or arbitrary action access.');
26
28
  const fixtures=value.tests===undefined?undefined:(bounded(value.tests,'Application source fixtures'),structuredClone(value.tests));
27
- const source={manifest,application:await defineBrowserApplication(value.application)};
29
+ const source={manifest,application:await defineBrowserApplication(value.application,{signal})};
30
+ signal?.throwIfAborted();const mapped=source.application.operations.filter(item=>Array.isArray(item.outputs));
31
+ if(mapped.length){
32
+ check(mapped.length===source.application.operations.length,'Every operation must map its output ports when any operation does.');
33
+ const exposed=new Set(mapped.flatMap(item=>item.outputs));
34
+ check(exposed.size===Object.keys(manifest.outputs).length&&[...exposed].every(name=>Object.hasOwn(manifest.outputs,name)),'Operation outputs must cover exactly the manifest output ports.');
35
+ for(const [name,port] of Object.entries(manifest.outputs))if(mapped.some(item=>!item.outputs.includes(name)))check(port.required===false,`Output ${name} must be optional because some operations do not produce it.`);
36
+ }
28
37
  if(fixtures!==undefined){
29
38
  check(Array.isArray(fixtures)&&fixtures.length>0&&fixtures.length<=50,'Supply 1–50 source fixtures when present.');
30
39
  source.tests=fixtures;
31
40
  for(const fixture of source.tests){
32
41
  check(exact(fixture,['name','operation','input','state','stateRevision','expected'])&&text(fixture.name),'Application fixtures declare name, operation, input, state, stateRevision and expected outputs.');
33
- operation(source,fixture.operation);contextState(fixture);validateValues(manifest.inputs,fixture.input);validateBlockOutputs(manifest,fixture.expected);
42
+ const selected=operation(source,fixture.operation);contextState(fixture);validateValues(manifest.inputs,fixture.input);validateBlockOutputs(manifest,fixture.expected);
43
+ if(selected.outputs)check(stableStringify(Object.keys(fixture.expected).sort())===stableStringify([...selected.outputs].sort()),'Fixture output ports differ from their operation contract.');
34
44
  }
35
45
  }
36
- freeze(source);verifiedSources.add(source);return source;
46
+ freeze(source);signal?.throwIfAborted();verifiedSources.add(source);return source;
37
47
  }
38
48
  function operation(source,id){const declared=source.application.operations.find(item=>item.id===id);check(declared,'Choose an operation declared by this exact application bundle.');return declared;}
39
49
  function contextState(options){check(plain(options)&&plain(options.state)&&text(options.stateRevision),'Supply host-owned saved state and its exact revision.');bounded(options.state,'Application state');return structuredClone(options.state);}
50
+ export async function getBrowserApplicationSourceDigests(value,{signal}={}){
51
+ signal?.throwIfAborted();
52
+ const source=await defineBrowserApplicationSource(value,{signal});
53
+ // Only the SDK's detached, deeply frozen source can key this private cache.
54
+ if(!sourceDigests.has(source))sourceDigests.set(source,Promise.all([digest(source),digest(source.application)]).then(([packageDigest,bundleDigest])=>Object.freeze({packageDigest,bundleDigest})));
55
+ const result=await sourceDigests.get(source);
56
+ signal?.throwIfAborted();return result;
57
+ }
40
58
  async function prepare(value,values,options,phase){
41
59
  bounded(values,'Application inputs');const inputCopy=structuredClone(values),state=contextState(options),revision=options.stateRevision,operationId=options.operation,interactive=options.interactive;
42
60
  const source=await defineBrowserApplicationSource(value),input=validateValues(source.manifest.inputs,inputCopy);
43
61
  const selected=phase==='execute'?operation(source,operationId):null;
44
62
  if(phase==='execute'&&source.application.lifecycle==='interactive')check(interactive===true,'Interactive application operations require an active interactive host.');
45
- // Cache only detached deeply frozen sources, never mutable caller data.
46
- if(!sourceDigests.has(source))sourceDigests.set(source,Promise.all([digest(source),digest(source.application)]));
47
- const [packageDigest,bundleDigest]=await sourceDigests.get(source);
48
- const request={schemaVersion:1,phase,requestId:globalThis.crypto.randomUUID(),profile:BROWSER_APPLICATION_PROFILE,packageDigest,bundleDigest,inputDigest:await digest(input),stateDigest:await digest(state),stateRevision:revision,operation:selected?structuredClone(selected):null,lifecycle:source.application.lifecycle};
63
+ const {packageDigest,bundleDigest}=await getBrowserApplicationSourceDigests(source);
64
+ const request={schemaVersion:1,phase,requestId:globalThis.crypto.randomUUID(),profile:source.application.profile,packageDigest,bundleDigest,inputDigest:await digest(input),stateDigest:await digest(state),stateRevision:revision,operation:selected?structuredClone(selected):null,lifecycle:source.application.lifecycle};
49
65
  const prepared=freeze({package:source,input,state,request});preparedRequests.add(prepared);return prepared;
50
66
  }
51
67
  export function prepareBrowserApplicationExecution(source,values,options){return prepare(source,values,options,'execute');}
@@ -68,6 +84,8 @@ export async function validateBrowserApplicationExecutionResult(prepared,result)
68
84
  const receipt=snapshot.applicationReceipt,request=validated.request;
69
85
  check(exact(receipt,['schemaVersion','phase','requestId','profile','packageDigest','bundleDigest','inputDigest','stateDigest','stateRevision','operation','lifecycle','outputDigest','executionId'])&&receipt.schemaVersion===1&&receipt.phase==='completed'&&text(receipt.executionId),'A mount or preview is not a completed application operation.');
70
86
  for(const key of Object.keys(request).filter(key=>key!=='phase'))check(stableStringify(receipt[key])===stableStringify(request[key]),'Application receipt does not match this exact execution request.');
87
+ check(plain(snapshot.outputs)&&Object.keys(snapshot.outputs).length>0,'A completed application operation must produce at least one declared output.');
88
+ if(request.operation.outputs)check(stableStringify(Object.keys(snapshot.outputs).sort())===stableStringify([...request.operation.outputs].sort()),'The operation returned different output ports than its host-declared mapping.');
71
89
  const outputs=validateBlockOutputs(validated.package.manifest,snapshot.outputs);
72
90
  check(receipt.outputDigest===await digest(outputs),'Application receipt does not match returned outputs.');
73
91
  return {outputs,applicationReceipt:receipt,independentlyVerified:false};
@@ -29,4 +29,6 @@ export function installBrowserApplicationGuest(options:{
29
29
  operations:ReadonlyArray<BrowserApplicationOperation>;
30
30
  /** May lower, never raise, the 4 MiB per-message limit. */
31
31
  messageBytes?:number;
32
+ /** Host-selected output-only ceiling, at most 8 MiB. Inputs and state keep messageBytes. */
33
+ outputBytes?:number;
32
34
  }):BrowserApplicationGuestController;
@@ -4,7 +4,7 @@
4
4
  * Object controls and cancellation are cooperative in the guest realm. The
5
5
  * trusted transport must enforce isolation, deadlines, UI-input freezing,
6
6
  * checkpoint identity, source bindings and actual worker termination itself. */
7
- export function installBrowserApplicationGuest({target=globalThis,operations,messageBytes=4194304}={}){
7
+ export function installBrowserApplicationGuest({target=globalThis,operations,messageBytes=4194304,outputBytes=messageBytes}={}){
8
8
  const fail=(code,message)=>Object.assign(new Error(message),{code});
9
9
  const check=(condition,code,message)=>{if(!condition)throw fail(code,message);};
10
10
  const plain=value=>value!==null&&typeof value==='object'&&!Array.isArray(value)&&[Object.prototype,null].includes(Object.getPrototypeOf(value));
@@ -20,23 +20,29 @@ export function installBrowserApplicationGuest({target=globalThis,operations,mes
20
20
  };
21
21
  check(target!==null&&['object','function'].includes(typeof target)&&!('stillmadeApplication' in target),'APPLICATION_GUEST_INSTALLED','Application registration is already installed or the target is invalid.');
22
22
  check(Number.isSafeInteger(messageBytes)&&messageBytes>0&&messageBytes<=4194304,'APPLICATION_GUEST_LIMIT','Application messages must be bounded by at most 4 MiB.');
23
+ check(Number.isSafeInteger(outputBytes)&&outputBytes>=messageBytes&&outputBytes<=8*1048576,'APPLICATION_GUEST_LIMIT','Application output candidates need a bounded host-selected limit.');
23
24
  check(Array.isArray(operations)&&operations.length>0&&operations.length<=32,'APPLICATION_GUEST_OPERATION','Declare 1–32 application operations.');
24
- const declared=new Set();
25
+ const declared=new Set(),mapped=new Map();
25
26
  for(const operation of operations){
26
- check(exact(operation,['id','kind']),'APPLICATION_GUEST_OPERATION','Declare only an operation id and kind.');
27
+ check(exact(operation,['id','kind'])||exact(operation,['id','kind','outputs']),'APPLICATION_GUEST_OPERATION','Declare an operation id, kind and optional output-port map.');
27
28
  const id=field(operation,'id'),kind=field(operation,'kind');
28
29
  check(typeof id==='string'&&/^[a-z][a-z0-9.-]{0,79}$/.test(id)&&!declared.has(id)&&['export','execute'].includes(kind),'APPLICATION_GUEST_OPERATION','Application operations must have distinct valid ids and kinds.');
30
+ if(Object.hasOwn(operation,'outputs')){
31
+ const names=field(operation,'outputs');
32
+ check(Array.isArray(names)&&names.length>0&&names.length<=64&&new Set(names).size===names.length&&names.every(name=>typeof name==='string'&&/^[a-z][a-zA-Z0-9_]*$/.test(name)),'APPLICATION_GUEST_OPERATION','Map the declared operation to distinct output ports.');
33
+ mapped.set(id,[...names]);
34
+ }
29
35
  declared.add(id);
30
36
  }
31
37
  // Copy JSON using data descriptors, never JSON.stringify on an adapter object
32
38
  // with accessors/toJSON. These checks catch mistakes; hostile guest code can
33
39
  // still change realm intrinsics, so the host must revalidate every message.
34
- function document(value,label){
40
+ function document(value,label,limit=messageBytes){
35
41
  let nodes=0;const ancestors=new Set();
36
42
  function visit(item,depth){
37
43
  check(depth<=40&&++nodes<=500000,'APPLICATION_GUEST_LIMIT',`${label} exceeds the JSON complexity limit.`);
38
44
  if(item===null||typeof item==='boolean')return item;
39
- if(typeof item==='string'){check(item.length<=messageBytes,'APPLICATION_GUEST_LIMIT',`${label} exceeds its message limit.`);return item;}
45
+ if(typeof item==='string'){check(item.length<=limit,'APPLICATION_GUEST_LIMIT',`${label} exceeds its message limit.`);return item;}
40
46
  if(typeof item==='number'){check(Number.isFinite(item),'APPLICATION_GUEST_DATA',`${label} needs finite numbers.`);return item;}
41
47
  check((Array.isArray(item)||plain(item))&&!ancestors.has(item),'APPLICATION_GUEST_DATA',`${label} needs acyclic plain JSON data.`);
42
48
  ancestors.add(item);let result;
@@ -47,7 +53,7 @@ export function installBrowserApplicationGuest({target=globalThis,operations,mes
47
53
  result={};
48
54
  for(const key of Reflect.ownKeys(item)){
49
55
  check(typeof key==='string'&&!['__proto__','prototype','constructor'].includes(key),'APPLICATION_GUEST_DATA',`${label} contains an unsupported field.`);
50
- check(key.length<=messageBytes,'APPLICATION_GUEST_LIMIT',`${label} exceeds its message limit.`);
56
+ check(key.length<=limit,'APPLICATION_GUEST_LIMIT',`${label} exceeds its message limit.`);
51
57
  result[key]=visit(field(item,key),depth+1);
52
58
  }
53
59
  }
@@ -55,7 +61,7 @@ export function installBrowserApplicationGuest({target=globalThis,operations,mes
55
61
  }
56
62
  check(plain(value),'APPLICATION_GUEST_DATA',`${label} must be a JSON object.`);
57
63
  const copied=visit(value,0);
58
- check(new TextEncoder().encode(JSON.stringify(copied)).length<=messageBytes,'APPLICATION_GUEST_LIMIT',`${label} exceeds its message limit.`);
64
+ check(new TextEncoder().encode(JSON.stringify(copied)).length<=limit,'APPLICATION_GUEST_LIMIT',`${label} exceeds its message limit.`);
59
65
  return copied;
60
66
  }
61
67
  const stable=value=>JSON.stringify((function sort(item){return Array.isArray(item)?item.map(sort):plain(item)?Object.fromEntries(Object.keys(item).sort().map(key=>[key,sort(item[key])])):item;})(value));
@@ -111,9 +117,10 @@ export function installBrowserApplicationGuest({target=globalThis,operations,mes
111
117
  const payload=document({operation,inputs,stateRevision},'Application operation');
112
118
  check(plain(payload.inputs)&&stable(payload.inputs)===mountedInputs,'APPLICATION_GUEST_INPUT','Operation inputs must match the mounted inputs.');
113
119
  return run('execute',signal,inner=>adapter.execute({...payload,signal:inner}),raw=>{
114
- const outputs=document(raw,'Application outputs');
120
+ const outputs=document(raw,'Application outputs',outputBytes);
115
121
  check(!['applicationReceipt','desktopReceipt','independentlyVerified'].some(key=>Object.hasOwn(outputs,key)),'APPLICATION_GUEST_RESULT','Adapters return output values only, never host receipt or verification fields.');
116
- return document({outputs},'Application result');
122
+ if(mapped.has(operation))check(JSON.stringify(Object.keys(outputs).sort())===JSON.stringify([...mapped.get(operation)].sort()),'APPLICATION_GUEST_RESULT','The adapter returned different output ports than this operation declares.');
123
+ return document({outputs},'Application result',outputBytes);
117
124
  });
118
125
  },
119
126
  dispose(){
@@ -1,13 +1,21 @@
1
1
  /** Source format only; this constant does not advertise an installed runtime. */
2
2
  export const BROWSER_APPLICATION_PROFILE:'desktop-browser-application/1';
3
- export const BROWSER_APPLICATION_LIMITS:Readonly<{assets:number;assetBytes:number;totalBytes:number;pathBytes:number;operations:number;messageBytes:number;timeoutMs:number}>;
4
- export type BrowserApplicationMime='text/html'|'text/javascript'|'text/css'|'application/json'|'application/manifest+json'|'application/xml'|'text/xml'|'application/wasm'|'application/octet-stream'|'text/plain'|'image/png'|'image/jpeg'|'image/gif'|'image/webp'|'image/avif'|'image/svg+xml'|'image/x-icon'|'font/woff2'|'font/woff'|'font/ttf'|'font/otf'|'audio/mpeg'|'audio/ogg'|'audio/wav'|'video/mp4'|'video/webm';
3
+ export const BROWSER_APPLICATION_EMBEDDED_PROFILE:'desktop-browser-application/2';
4
+ export const BROWSER_APPLICATION_LOCAL_WORKER_PROFILE:'desktop-browser-application/3';
5
+ export const BROWSER_APPLICATION_LIMITS:Readonly<{assets:number;assetBytes:number;totalBytes:number;pathBytes:number;operations:number;messageBytes:number;outputCandidateBytes:number;timeoutMs:number}>;
6
+ export type BrowserApplicationMime='text/html'|'text/javascript'|'text/css'|'application/json'|'application/manifest+json'|'application/xml'|'text/xml'|'application/wasm'|'application/octet-stream'|'text/plain'|'image/png'|'image/jpeg'|'image/gif'|'image/webp'|'image/avif'|'image/svg+xml'|'image/x-icon'|'font/woff2'|'font/woff'|'font/ttf'|'font/otf'|'application/vnd.ms-fontobject'|'audio/mpeg'|'audio/ogg'|'audio/wav'|'video/mp4'|'video/webm';
5
7
  export interface BrowserApplicationAsset {path:string;mime:BrowserApplicationMime;sha256:string;encoding:'base64';bytes:number;data:string}
6
8
  export interface BrowserApplicationOperation {id:string;kind:'export'|'execute'}
7
- export interface BrowserApplicationBundle {
9
+ export type BrowserApplicationBundle={
8
10
  schemaVersion:1;profile:typeof BROWSER_APPLICATION_PROFILE;appearance:'original';entry:string;adapter:string;
9
11
  lifecycle:'interactive'|'headless';operations:BrowserApplicationOperation[];assets:BrowserApplicationAsset[];features:{wasm:boolean};
10
- }
12
+ }|{
13
+ schemaVersion:1;profile:typeof BROWSER_APPLICATION_EMBEDDED_PROFILE;appearance:'original';entry:string;adapter:string;
14
+ lifecycle:'interactive'|'headless';operations:BrowserApplicationOperation[];assets:BrowserApplicationAsset[];features:{wasm:boolean;srcdocFrames:boolean};
15
+ }|{
16
+ schemaVersion:1;profile:typeof BROWSER_APPLICATION_LOCAL_WORKER_PROFILE;appearance:'original';entry:string;adapter:string;
17
+ lifecycle:'interactive'|'headless';operations:BrowserApplicationOperation[];assets:BrowserApplicationAsset[];features:{wasm:boolean;srcdocFrames:boolean;localWorkers:true};
18
+ };
11
19
  export type BrowserApplicationDeepReadonly<T>=T extends object?{readonly [P in keyof T]:BrowserApplicationDeepReadonly<T[P]>}:T;
12
20
  /** Strict canonical relative regular-file path. No URL, traversal or decoding. */
13
21
  export function browserApplicationPath(path:string):string;
@@ -3,15 +3,17 @@ import {assert,plain,safeDocument,digest} from './contracts.js';
3
3
  // A bundle/evidence format, not an installed runtime or an admission decision.
4
4
  // The existing restricted view and portable archive formats are unchanged.
5
5
  export const BROWSER_APPLICATION_PROFILE='desktop-browser-application/1';
6
+ export const BROWSER_APPLICATION_EMBEDDED_PROFILE='desktop-browser-application/2';
7
+ export const BROWSER_APPLICATION_LOCAL_WORKER_PROFILE='desktop-browser-application/3';
6
8
  // One large WASM engine plus fonts, icons and editor resources, with explicit
7
9
  // aggregate bounds. These are not the existing portable archive's 4 MB limit.
8
- export const BROWSER_APPLICATION_LIMITS=Object.freeze({assets:2048,assetBytes:64*1024*1024,totalBytes:128*1024*1024,pathBytes:256,operations:32,messageBytes:4*1024*1024,timeoutMs:120000});
10
+ export const BROWSER_APPLICATION_LIMITS=Object.freeze({assets:2048,assetBytes:64*1024*1024,totalBytes:128*1024*1024,pathBytes:256,operations:32,messageBytes:4*1024*1024,outputCandidateBytes:8*1024*1024,timeoutMs:120000});
9
11
  const mimeExtensions=Object.freeze({
10
12
  'text/html':['html'],'text/javascript':['js','mjs'],'text/css':['css'],'application/json':['json','map'],
11
13
  'application/manifest+json':['webmanifest'],'application/xml':['xml'],'text/xml':['xml'],
12
14
  'application/wasm':['wasm'],'application/octet-stream':['bin','data','dat'],'text/plain':['txt'],
13
15
  'image/png':['png'],'image/jpeg':['jpg','jpeg'],'image/gif':['gif'],'image/webp':['webp'],'image/avif':['avif'],'image/svg+xml':['svg'],'image/x-icon':['ico'],
14
- 'font/woff2':['woff2'],'font/woff':['woff'],'font/ttf':['ttf'],'font/otf':['otf'],
16
+ 'font/woff2':['woff2'],'font/woff':['woff'],'font/ttf':['ttf'],'font/otf':['otf'],'application/vnd.ms-fontobject':['eot'],
15
17
  'audio/mpeg':['mp3'],'audio/ogg':['ogg'],'audio/wav':['wav'],'video/mp4':['mp4'],'video/webm':['webm'],
16
18
  });
17
19
  const verifiedBundles=new WeakSet();
@@ -20,20 +22,23 @@ const hash=value=>typeof value==='string'&&/^[a-f0-9]{64}$/.test(value);
20
22
  const freeze=value=>{if(value&&typeof value==='object'){Object.values(value).forEach(freeze);Object.freeze(value);}return value;};
21
23
  const check=(condition,message)=>assert(condition,'BROWSER_APPLICATION',message);
22
24
  export function browserApplicationPath(path){
23
- check(typeof path==='string'&&path.length>0&&path.length<=BROWSER_APPLICATION_LIMITS.pathBytes&&/^[A-Za-z0-9_@$-][A-Za-z0-9_@$./-]*$/.test(path),'Use a bounded relative application asset path, never a URL.');
25
+ check(typeof path==='string'&&path.length>0&&path.length<=BROWSER_APPLICATION_LIMITS.pathBytes&&/^[A-Za-z0-9_@$+-][A-Za-z0-9_@$./&+-]*$/.test(path),'Use a bounded relative application asset path, never a URL.');
24
26
  check(path.split('/').every(part=>part&&part!=='.'&&part!=='..'&&!part.endsWith('.')&&!/^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\.|$)/i.test(part)),'Application paths must be canonical regular-file paths.');
25
27
  return path;
26
28
  }
27
29
  function metadata(value){
28
30
  safeDocument(value);
29
- check(exact(value,['schemaVersion','profile','appearance','entry','adapter','lifecycle','operations','assets','features'])&&value.schemaVersion===1&&value.profile===BROWSER_APPLICATION_PROFILE,'Use desktop browser application bundle schema 1.');
31
+ check(exact(value,['schemaVersion','profile','appearance','entry','adapter','lifecycle','operations','assets','features'])&&value.schemaVersion===1&&[BROWSER_APPLICATION_PROFILE,BROWSER_APPLICATION_EMBEDDED_PROFILE,BROWSER_APPLICATION_LOCAL_WORKER_PROFILE].includes(value.profile),'Use a supported desktop browser application bundle profile.');
30
32
  check(value.appearance==='original','This profile preserves original styling; host theming requires a separately declared future adapter.');
31
33
  check(['interactive','headless'].includes(value.lifecycle),'Declare an interactive or headless application lifecycle.');
32
- check(exact(value.features,['wasm'])&&typeof value.features.wasm==='boolean','Declare whether this bundle needs single-thread WebAssembly; workers and ambient networking are unavailable.');
34
+ const embedded=value.profile===BROWSER_APPLICATION_EMBEDDED_PROFILE;
35
+ const localWorkers=value.profile===BROWSER_APPLICATION_LOCAL_WORKER_PROFILE;
36
+ check(exact(value.features,localWorkers?['wasm','srcdocFrames','localWorkers']:embedded?['wasm','srcdocFrames']:['wasm'])&&typeof value.features.wasm==='boolean'&&(!embedded&&!localWorkers||typeof value.features.srcdocFrames==='boolean')&&(!localWorkers||value.features.localWorkers===true),'Declare browser features for the exact profile; ambient networking is unavailable.');
33
37
  check(Array.isArray(value.operations)&&value.operations.length>0&&value.operations.length<=BROWSER_APPLICATION_LIMITS.operations,'Declare 1–32 output-producing adapter operations.');
34
38
  const operations=new Set();for(const operation of value.operations){
35
- check(exact(operation,['id','kind'])&&typeof operation.id==='string'&&/^[a-z][a-z0-9.-]{0,79}$/.test(operation.id)&&!operations.has(operation.id),'Use distinct named application operations.');
39
+ check((exact(operation,['id','kind'])||exact(operation,['id','kind','outputs']))&&typeof operation.id==='string'&&/^[a-z][a-z0-9.-]{0,79}$/.test(operation.id)&&!operations.has(operation.id),'Use distinct named application operations.');
36
40
  check(['export','execute'].includes(operation.kind)&&(value.lifecycle!=='headless'||operation.kind==='execute'),'A headless application must execute without an interactive export.');operations.add(operation.id);
41
+ if(Object.hasOwn(operation,'outputs'))check(Array.isArray(operation.outputs)&&operation.outputs.length>0&&operation.outputs.length<=64&&new Set(operation.outputs).size===operation.outputs.length&&operation.outputs.every(name=>typeof name==='string'&&/^[a-z][a-zA-Z0-9_]*$/.test(name)),'Map each operation to distinct bounded output ports.');
37
42
  }
38
43
  check(Array.isArray(value.assets)&&value.assets.length>=2&&value.assets.length<=BROWSER_APPLICATION_LIMITS.assets,'Declare 2–2048 bundled regular-file assets.');
39
44
  const paths=new Set();let total=0,wasm=false;
@@ -64,16 +69,25 @@ function decode(asset){
64
69
  async function bytesDigest(bytes){return Array.from(new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256',bytes)),byte=>byte.toString(16).padStart(2,'0')).join('');}
65
70
  /** Checks hashes and returns immutable detached data. This never inspects code
66
71
  * for safety, performs a build, starts a browser, or grants any capability. */
67
- export async function defineBrowserApplication(value){
72
+ export async function defineBrowserApplication(value,{signal}={}){
73
+ signal?.throwIfAborted();
68
74
  if(verifiedBundles.has(value))return value;
69
75
  metadata(value);const bundle=structuredClone(value);
70
- for(const asset of bundle.assets){
71
- const bytes=decode(asset);check(await bytesDigest(bytes)===asset.sha256,`Application asset integrity failed: ${asset.path}`);
72
- if(['text/html','text/javascript','text/css','application/json','application/manifest+json','application/xml','text/xml','text/plain','image/svg+xml'].includes(asset.mime)){
73
- try{new TextDecoder('utf-8',{fatal:true}).decode(bytes);}catch{check(false,`Application text asset is not UTF-8: ${asset.path}`);}
76
+ let nextAsset=0,failed=false,firstFailure;
77
+ const verifyAssets=async()=>{
78
+ while(!failed&&nextAsset<bundle.assets.length){
79
+ try{
80
+ signal?.throwIfAborted();const asset=bundle.assets[nextAsset++],bytes=decode(asset);signal?.throwIfAborted();check(await bytesDigest(bytes)===asset.sha256,`Application asset integrity failed: ${asset.path}`);signal?.throwIfAborted();
81
+ if(['text/html','text/javascript','text/css','application/json','application/manifest+json','application/xml','text/xml','text/plain','image/svg+xml'].includes(asset.mime)){
82
+ try{new TextDecoder('utf-8',{fatal:true}).decode(bytes);}catch{check(false,`Application text asset is not UTF-8: ${asset.path}`);}
83
+ }
84
+ }catch(error){if(!failed){failed=true;firstFailure=error;}}
74
85
  }
75
- }
76
- freeze(bundle);verifiedBundles.add(bundle);return bundle;
86
+ };
87
+ // Drain every started verification before rejection or immutable publication.
88
+ await Promise.all(Array.from({length:Math.min(4,bundle.assets.length)},verifyAssets));
89
+ if(failed)throw firstFailure;
90
+ signal?.throwIfAborted();freeze(bundle);verifiedBundles.add(bundle);return bundle;
77
91
  }
78
92
  /** Exact bundle lookup only: no URL resolution, filesystem access or fetch.
79
93
  * Hosts must independently deny navigation, network and unlisted resources. */