@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
|
|
303
|
-
5-second settlement skew. A Domain credential never outlives its
|
|
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
|
-
|
|
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;
|