@astrale-os/sdk 0.6.0-beta.11 → 0.6.0-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -289,7 +289,7 @@ const session = connect({
289
289
  // Return { credential, expiresAt } with expiresAt in Unix milliseconds.
290
290
  mint: (signal) => mintSourceCredential(signal),
291
291
  }),
292
- exchange: {},
292
+ exchange: { mode: 'auto' },
293
293
  })
294
294
 
295
295
  await session.call({ target: '/:orders.example:class.Order:create', input }) // Domain callable
@@ -299,8 +299,10 @@ await session.graph.get('@order-id') // Kernel callable: the source credential
299
299
  - **Hold the source in a provider.** Domain credentials are cached per source credential, so a
300
300
  `resolve` that returns a new token on every call also exchanges on every call. The provider also
301
301
  lets the Session renew a source that is too close to expiry to feed a Domain credential.
302
- - **Lifetimes.** `exchange.ttlSeconds` (default 300) must exceed the provider's `ttlSeconds` plus a
303
- 5-second settlement skew. A Domain credential never outlives its source.
302
+ - **Lifetimes.** In `exchange: { mode: 'auto', ttlSeconds }`, `ttlSeconds` (default 300) must exceed
303
+ the provider's `ttlSeconds` plus a 5-second settlement skew. A Domain credential never outlives its
304
+ source. `exchange: { mode: 'source' }` keeps the source credential for every Call and takes no
305
+ `ttlSeconds`; an empty `{}` or a bare `{ ttlSeconds }` is rejected.
304
306
  - **Reading as a Domain.** To call Kernel callables as a Domain, for example to read Shell-owned
305
307
  Classes, feed a second Session from an explicit exchange:
306
308
  `createSessionCredentialProvider({ ttlSeconds: 60, mint: (signal) => session.exchange(shellIssuer, { signal }) })`.
@@ -2,6 +2,10 @@ import { InvocationDefect } from './errors.js';
2
2
  import { validateOutput } from './validate.js';
3
3
  const MEDIA_TYPE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/u;
4
4
  const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/u;
5
+ const BUFFERED_FIELDS = new Set(['body', 'mediaType', 'status', 'headers']);
6
+ const STREAMING_FIELDS = new Set([...BUFFERED_FIELDS, 'length', 'delivery']);
7
+ /** Largest delay a host timer accepts; the Server rejects a longer delivery budget. */
8
+ const MAXIMUM_TIMER_MS = 2_147_483_647;
5
9
  export function project(callable, output) {
6
10
  switch (callable.output.mode) {
7
11
  case 'value':
@@ -72,7 +76,8 @@ function binary(input) {
72
76
  if (input === null || typeof input !== 'object' || Array.isArray(input))
73
77
  invalidBinary();
74
78
  const value = input;
75
- if (Reflect.ownKeys(input).some((key) => key !== 'body' && key !== 'mediaType' && key !== 'status' && key !== 'headers') ||
79
+ const fields = isBytes(value.body) ? BUFFERED_FIELDS : STREAMING_FIELDS;
80
+ if (Reflect.ownKeys(input).some((key) => typeof key !== 'string' || !fields.has(key)) ||
76
81
  (!isBytes(value.body) && !isAsyncBytes(value.body)) ||
77
82
  typeof value.mediaType !== 'string' ||
78
83
  !MEDIA_TYPE.test(value.mediaType) ||
@@ -89,13 +94,48 @@ function binary(input) {
89
94
  ...(headers === undefined ? {} : { headers }),
90
95
  });
91
96
  }
97
+ const length = streamingLength(value.length);
98
+ const delivery = deliveryBudget(value.delivery);
92
99
  return Object.freeze({
93
100
  body: new InvocationByteStream(value.body),
94
101
  mediaType: value.mediaType,
102
+ // The declared length is the only count a Client verifies a streaming delivery against.
103
+ ...(length === undefined ? {} : { length }),
104
+ ...(delivery === undefined ? {} : { delivery }),
95
105
  ...(value.status === undefined ? {} : { status: value.status }),
96
106
  ...(headers === undefined ? {} : { headers }),
97
107
  });
98
108
  }
109
+ function streamingLength(input) {
110
+ if (input === undefined)
111
+ return undefined;
112
+ if (typeof input !== 'number' || !Number.isSafeInteger(input) || input < 0)
113
+ invalidBinary();
114
+ return input;
115
+ }
116
+ function deliveryBudget(input) {
117
+ if (input === undefined)
118
+ return undefined;
119
+ if (input === null || typeof input !== 'object' || Array.isArray(input))
120
+ invalidBinary();
121
+ const value = input;
122
+ if (Reflect.ownKeys(input).some((key) => key !== 'maximumDurationMs' && key !== 'idleTimeoutMs') ||
123
+ !isTimerDuration(value.maximumDurationMs) ||
124
+ !isTimerDuration(value.idleTimeoutMs) ||
125
+ value.idleTimeoutMs > value.maximumDurationMs) {
126
+ invalidBinary();
127
+ }
128
+ return Object.freeze({
129
+ maximumDurationMs: value.maximumDurationMs,
130
+ idleTimeoutMs: value.idleTimeoutMs,
131
+ });
132
+ }
133
+ function isTimerDuration(input) {
134
+ return (typeof input === 'number' &&
135
+ Number.isSafeInteger(input) &&
136
+ input > 0 &&
137
+ input <= MAXIMUM_TIMER_MS);
138
+ }
99
139
  class InvocationByteStream {
100
140
  source;
101
141
  consumed = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/sdk",
3
- "version": "0.6.0-beta.11",
3
+ "version": "0.6.0-beta.12",
4
4
  "description": "Schema-first SDK for defining, composing, and deploying Astrale domains",
5
5
  "keywords": [
6
6
  "astrale",