@adea-ai/spatial 0.98.0 → 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -35,6 +35,15 @@ export type SimEntitlementChecker = (query: SimEntitlementQuery) => MaybePromise
|
|
|
35
35
|
export type SimLaunchCodeStore = Readonly<{
|
|
36
36
|
issue(code: string, binding: LaunchCodeBinding, expiresAt: number): MaybePromise<void>;
|
|
37
37
|
redeem(code: string, presented: Omit<LaunchCodeBinding, 'surface'>, now: number): MaybePromise<LaunchCodeRedemption>;
|
|
38
|
+
/**
|
|
39
|
+
* Optional atomic form of redeem-then-save. A store that provides it spends the code and saves the
|
|
40
|
+
* session in one step: when saving fails, the spend is rolled back and the code stays redeemable. The
|
|
41
|
+
* session surface is the code's bound surface. A store without it uses redeem, then sessions.save.
|
|
42
|
+
*/
|
|
43
|
+
redeemAndSave?(code: string, presented: Omit<LaunchCodeBinding, 'surface'>, now: number, session: Readonly<{
|
|
44
|
+
value: string;
|
|
45
|
+
expiresAt: number;
|
|
46
|
+
}>): MaybePromise<LaunchCodeRedemption>;
|
|
38
47
|
}>;
|
|
39
48
|
export type SimSessionRecord = Readonly<{
|
|
40
49
|
surface: SimSurface;
|
|
@@ -99,19 +99,31 @@ export function createSimSessionExchange(config) {
|
|
|
99
99
|
}
|
|
100
100
|
if (!expiresAtIso)
|
|
101
101
|
return { body: { status: 'unavailable' } };
|
|
102
|
+
const presented = { bridgeId: request.bridgeId, origin };
|
|
102
103
|
let redemption;
|
|
104
|
+
if (config.store.redeemAndSave) {
|
|
105
|
+
// One atomic step: a failed session save rolls the spend back, so the code stays redeemable.
|
|
106
|
+
try {
|
|
107
|
+
redemption = await config.store.redeemAndSave(request.code, presented, at, {
|
|
108
|
+
value,
|
|
109
|
+
expiresAt,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return { body: { status: 'unavailable' } };
|
|
114
|
+
}
|
|
115
|
+
if (!redemption.ok)
|
|
116
|
+
return { body: rejectedBody(redemption.reason) };
|
|
117
|
+
return { body: { status: 'session', expiresAt: expiresAtIso }, setCookie };
|
|
118
|
+
}
|
|
103
119
|
try {
|
|
104
|
-
redemption = await config.store.redeem(request.code,
|
|
120
|
+
redemption = await config.store.redeem(request.code, presented, at);
|
|
105
121
|
}
|
|
106
122
|
catch {
|
|
107
123
|
return { body: { status: 'unavailable' } };
|
|
108
124
|
}
|
|
109
|
-
if (!redemption.ok)
|
|
110
|
-
|
|
111
|
-
// whether a code ever existed.
|
|
112
|
-
const reason = redemption.reason === 'unknown' ? 'code_expired' : redemption.reason;
|
|
113
|
-
return { body: { status: 'rejected', reason } };
|
|
114
|
-
}
|
|
125
|
+
if (!redemption.ok)
|
|
126
|
+
return { body: rejectedBody(redemption.reason) };
|
|
115
127
|
// The code is spent from here on: if saving fails the same code is not retryable, and the client
|
|
116
128
|
// starts a new launch.
|
|
117
129
|
try {
|
|
@@ -123,6 +135,12 @@ export function createSimSessionExchange(config) {
|
|
|
123
135
|
return { body: { status: 'session', expiresAt: expiresAtIso }, setCookie };
|
|
124
136
|
};
|
|
125
137
|
}
|
|
138
|
+
/** A refused redemption, with the reason the reply may carry. */
|
|
139
|
+
function rejectedBody(reason) {
|
|
140
|
+
// An unknown code carries no more information than an expired one; the reply never says
|
|
141
|
+
// whether a code ever existed.
|
|
142
|
+
return { status: 'rejected', reason: reason === 'unknown' ? 'code_expired' : reason };
|
|
143
|
+
}
|
|
126
144
|
/**
|
|
127
145
|
* A configured lifetime in the unit the caller passes (milliseconds or seconds): the default when
|
|
128
146
|
* absent, clamped to [1, max] when a finite number, and null for anything else (NaN, Infinity, a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adea-ai/spatial",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.0",
|
|
4
4
|
"description": "Scene and asset manifest contracts plus the shell-to-engine spatial protocol for Adea workspaces: scene mount manifests, spawn URL helpers, and scene telemetry schemas. Pure types — no rendering, no binaries.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"assets",
|
|
@@ -59,6 +59,17 @@ export type SimLaunchCodeStore = Readonly<{
|
|
|
59
59
|
presented: Omit<LaunchCodeBinding, 'surface'>,
|
|
60
60
|
now: number
|
|
61
61
|
): MaybePromise<LaunchCodeRedemption>
|
|
62
|
+
/**
|
|
63
|
+
* Optional atomic form of redeem-then-save. A store that provides it spends the code and saves the
|
|
64
|
+
* session in one step: when saving fails, the spend is rolled back and the code stays redeemable. The
|
|
65
|
+
* session surface is the code's bound surface. A store without it uses redeem, then sessions.save.
|
|
66
|
+
*/
|
|
67
|
+
redeemAndSave?(
|
|
68
|
+
code: string,
|
|
69
|
+
presented: Omit<LaunchCodeBinding, 'surface'>,
|
|
70
|
+
now: number,
|
|
71
|
+
session: Readonly<{ value: string; expiresAt: number }>
|
|
72
|
+
): MaybePromise<LaunchCodeRedemption>
|
|
62
73
|
}>
|
|
63
74
|
|
|
64
75
|
export type SimSessionRecord = Readonly<{ surface: SimSurface; expiresAt: number }>
|
|
@@ -209,22 +220,28 @@ export function createSimSessionExchange(
|
|
|
209
220
|
}
|
|
210
221
|
if (!expiresAtIso) return { body: { status: 'unavailable' } }
|
|
211
222
|
|
|
223
|
+
const presented = { bridgeId: request.bridgeId, origin }
|
|
212
224
|
let redemption: LaunchCodeRedemption
|
|
225
|
+
if (config.store.redeemAndSave) {
|
|
226
|
+
// One atomic step: a failed session save rolls the spend back, so the code stays redeemable.
|
|
227
|
+
try {
|
|
228
|
+
redemption = await config.store.redeemAndSave(request.code, presented, at, {
|
|
229
|
+
value,
|
|
230
|
+
expiresAt,
|
|
231
|
+
})
|
|
232
|
+
} catch {
|
|
233
|
+
return { body: { status: 'unavailable' } }
|
|
234
|
+
}
|
|
235
|
+
if (!redemption.ok) return { body: rejectedBody(redemption.reason) }
|
|
236
|
+
return { body: { status: 'session', expiresAt: expiresAtIso }, setCookie }
|
|
237
|
+
}
|
|
238
|
+
|
|
213
239
|
try {
|
|
214
|
-
redemption = await config.store.redeem(
|
|
215
|
-
request.code,
|
|
216
|
-
{ bridgeId: request.bridgeId, origin },
|
|
217
|
-
at
|
|
218
|
-
)
|
|
240
|
+
redemption = await config.store.redeem(request.code, presented, at)
|
|
219
241
|
} catch {
|
|
220
242
|
return { body: { status: 'unavailable' } }
|
|
221
243
|
}
|
|
222
|
-
if (!redemption.ok) {
|
|
223
|
-
// An unknown code carries no more information than an expired one; the reply never says
|
|
224
|
-
// whether a code ever existed.
|
|
225
|
-
const reason = redemption.reason === 'unknown' ? 'code_expired' : redemption.reason
|
|
226
|
-
return { body: { status: 'rejected', reason } }
|
|
227
|
-
}
|
|
244
|
+
if (!redemption.ok) return { body: rejectedBody(redemption.reason) }
|
|
228
245
|
|
|
229
246
|
// The code is spent from here on: if saving fails the same code is not retryable, and the client
|
|
230
247
|
// starts a new launch.
|
|
@@ -237,6 +254,13 @@ export function createSimSessionExchange(
|
|
|
237
254
|
}
|
|
238
255
|
}
|
|
239
256
|
|
|
257
|
+
/** A refused redemption, with the reason the reply may carry. */
|
|
258
|
+
function rejectedBody(reason: 'unknown' | 'code_expired' | 'code_used' | 'origin_refused') {
|
|
259
|
+
// An unknown code carries no more information than an expired one; the reply never says
|
|
260
|
+
// whether a code ever existed.
|
|
261
|
+
return { status: 'rejected' as const, reason: reason === 'unknown' ? 'code_expired' : reason }
|
|
262
|
+
}
|
|
263
|
+
|
|
240
264
|
/**
|
|
241
265
|
* A configured lifetime in the unit the caller passes (milliseconds or seconds): the default when
|
|
242
266
|
* absent, clamped to [1, max] when a finite number, and null for anything else (NaN, Infinity, a
|