@tinyburg/treasury-sdk 0.0.0-stage → 0.2.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.
- package/LICENSE +674 -0
- package/README.md +4 -2
- package/dist/Scopes.d.ts +53 -0
- package/dist/Scopes.d.ts.map +1 -0
- package/dist/Scopes.js +54 -0
- package/dist/Scopes.js.map +1 -0
- package/dist/Sdk.d.ts +1264 -0
- package/dist/Sdk.d.ts.map +1 -0
- package/dist/Sdk.js +328 -0
- package/dist/Sdk.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -3
- package/src/Scopes.ts +70 -0
- package/src/Sdk.ts +387 -0
- package/src/index.ts +52 -0
package/src/Sdk.ts
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The api the treasury serves: player-to-player trades, escrowed.
|
|
3
|
+
*
|
|
4
|
+
* Nimblebit has no hold primitive, so the treasury makes one out of the gift
|
|
5
|
+
* channel: each side's item is sent to a treasury-owned vault tower, where it
|
|
6
|
+
* sits as an *unreceived* gift - a native escrow slot - until both deposits
|
|
7
|
+
* are verified, and only then is each item forwarded to the other side. A
|
|
8
|
+
* save-splice trade instead rewrites both saves directly, which is why it
|
|
9
|
+
* requires an explicit confirmation from both parties before anything runs.
|
|
10
|
+
*
|
|
11
|
+
* Every endpoint but the public stats is bearer authenticated with a JWT
|
|
12
|
+
* access token the Tinyburg OAuth server issued for the Tinyburg api
|
|
13
|
+
* (`<site>/v1`), and guarded by a leaf of the `treasury` scope area in
|
|
14
|
+
* `Scopes`. The treasury lives behind tinyburg.app: the paths here are
|
|
15
|
+
* reached at `<site>/v1/treasury/*`, which tinyburg.app forwards verbatim,
|
|
16
|
+
* `Authorization` header included. The first-party app gets its token from
|
|
17
|
+
* tinyburg.app's `/auth/token`; any other application uses the one it was
|
|
18
|
+
* issued.
|
|
19
|
+
*
|
|
20
|
+
* A token without the endpoint's scope is refused with `403` and an
|
|
21
|
+
* `insufficient_scope` challenge. Any other `403` from a trade endpoint
|
|
22
|
+
* means the treasury holds no usable grant for the caller's towers;
|
|
23
|
+
* `GrantStatus` says so explicitly and carries the url that starts the
|
|
24
|
+
* connect flow.
|
|
25
|
+
*
|
|
26
|
+
* @since 1.0.0
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup } from "effect/http-api";
|
|
30
|
+
import * as Schema from "effect/Schema";
|
|
31
|
+
|
|
32
|
+
import * as NimblebitConfig from "@tinyburg/nimblebit-sdk/NimblebitConfig";
|
|
33
|
+
import * as NimblebitScopes from "@tinyburg/nimblebit-sdk/NimblebitScopes";
|
|
34
|
+
import * as SyncItemType from "@tinyburg/tinytower-sdk/SyncItemType";
|
|
35
|
+
import * as OAuthScope from "@yielded/auth/OAuthScope";
|
|
36
|
+
|
|
37
|
+
import * as Scopes from "./Scopes.ts";
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Schemas
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* How a trade moves its items. `gift` rides Nimblebit's gift channel through
|
|
45
|
+
* the vault; `splice` rewrites both saves and can carry what the gift channel
|
|
46
|
+
* cannot (costumes, pets, coins, bux).
|
|
47
|
+
*
|
|
48
|
+
* @since 1.0.0
|
|
49
|
+
* @category Schemas
|
|
50
|
+
*/
|
|
51
|
+
export const TradeMechanism = Schema.Literals(["gift", "splice"]);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The games the treasury trades in. TinyTower only, until the tinyburg api
|
|
55
|
+
* serves more.
|
|
56
|
+
*
|
|
57
|
+
* @since 1.0.0
|
|
58
|
+
* @category Schemas
|
|
59
|
+
*/
|
|
60
|
+
export const TradeGame = Schema.Literals(["tinytower"]);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Where a trade is in its life. The happy path is `proposed` → `accepted` →
|
|
64
|
+
* (`awaiting_confirmation`, splice only) → `escrowing` → `escrowed` →
|
|
65
|
+
* `releasing` (gift) or `executing` (splice) → `settled`; everything else is
|
|
66
|
+
* one of the ways out.
|
|
67
|
+
*
|
|
68
|
+
* @since 1.0.0
|
|
69
|
+
* @category Schemas
|
|
70
|
+
*/
|
|
71
|
+
export const TradeState = Schema.Literals([
|
|
72
|
+
"proposed",
|
|
73
|
+
"accepted",
|
|
74
|
+
"awaiting_confirmation",
|
|
75
|
+
"escrowing",
|
|
76
|
+
"escrowed",
|
|
77
|
+
"releasing",
|
|
78
|
+
"executing",
|
|
79
|
+
"settled",
|
|
80
|
+
"cancelled",
|
|
81
|
+
"refunding",
|
|
82
|
+
"refunded",
|
|
83
|
+
"expired",
|
|
84
|
+
"failed",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* One side of a gift trade's journey through the vault: `pending` → `sent`
|
|
89
|
+
* (deposit dispatched) → `verified` (seen in the vault's gift queue) →
|
|
90
|
+
* `released` (forwarded to the counterparty) → `settled` (vault slot
|
|
91
|
+
* claimed); refunds run `refund_sent` → `refunded`, and `lost` is a deposit
|
|
92
|
+
* that never arrived.
|
|
93
|
+
*
|
|
94
|
+
* @since 1.0.0
|
|
95
|
+
* @category Schemas
|
|
96
|
+
*/
|
|
97
|
+
export const LegState = Schema.Literals([
|
|
98
|
+
"pending",
|
|
99
|
+
"sent",
|
|
100
|
+
"verified",
|
|
101
|
+
"released",
|
|
102
|
+
"settled",
|
|
103
|
+
"refund_sent",
|
|
104
|
+
"refunded",
|
|
105
|
+
"lost",
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @since 1.0.0
|
|
110
|
+
* @category Schemas
|
|
111
|
+
*/
|
|
112
|
+
export const TradeRole = Schema.Literals(["proposer", "counterparty"]);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* One item offered over the gift channel, in the encoded form the game
|
|
116
|
+
* exchanges items in - the same string `SendItem` carries.
|
|
117
|
+
*
|
|
118
|
+
* @since 1.0.0
|
|
119
|
+
* @category Schemas
|
|
120
|
+
*/
|
|
121
|
+
export const GiftItem = Schema.Struct({
|
|
122
|
+
_tag: Schema.tag("GiftItem"),
|
|
123
|
+
itemType: Schema.Enum(SyncItemType.SyncItemType),
|
|
124
|
+
item: Schema.String,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* One item offered by splice. Bitizens, costumes and pets carry the encoded
|
|
129
|
+
* string a save stores them as; coins and bux carry an amount instead.
|
|
130
|
+
*
|
|
131
|
+
* @since 1.0.0
|
|
132
|
+
* @category Schemas
|
|
133
|
+
*/
|
|
134
|
+
export const SpliceItem = Schema.Struct({
|
|
135
|
+
kind: Schema.Literals(["bitizen", "costume", "pet", "coins", "bux"]),
|
|
136
|
+
item: Schema.optional(Schema.String),
|
|
137
|
+
amount: Schema.optional(Schema.Int),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @since 1.0.0
|
|
142
|
+
* @category Schemas
|
|
143
|
+
*/
|
|
144
|
+
export const SpliceItems = Schema.Struct({
|
|
145
|
+
_tag: Schema.tag("SpliceItems"),
|
|
146
|
+
items: Schema.Array(SpliceItem),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* What one side of a trade puts in: a gift-channel item or a list of splice
|
|
151
|
+
* items, matching the trade's mechanism.
|
|
152
|
+
*
|
|
153
|
+
* @since 1.0.0
|
|
154
|
+
* @category Schemas
|
|
155
|
+
*/
|
|
156
|
+
export const ItemSpec = Schema.Union([GiftItem, SpliceItems]);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A trade as the api describes it. `gives` and `wants` are always from the
|
|
160
|
+
* proposer's point of view; `role` is which side the caller is on, absent for
|
|
161
|
+
* an open offer the caller could accept.
|
|
162
|
+
*
|
|
163
|
+
* @since 1.0.0
|
|
164
|
+
* @category Schemas
|
|
165
|
+
*/
|
|
166
|
+
export const Trade = Schema.Struct({
|
|
167
|
+
id: Schema.String,
|
|
168
|
+
mechanism: TradeMechanism,
|
|
169
|
+
game: TradeGame,
|
|
170
|
+
state: TradeState,
|
|
171
|
+
proposerPlayerId: NimblebitConfig.PlayerIdSchema,
|
|
172
|
+
counterpartyPlayerId: Schema.optional(NimblebitConfig.PlayerIdSchema),
|
|
173
|
+
gives: ItemSpec,
|
|
174
|
+
wants: ItemSpec,
|
|
175
|
+
role: Schema.optional(TradeRole),
|
|
176
|
+
proposerConfirmedAt: Schema.optional(Schema.DateTimeUtcFromString),
|
|
177
|
+
counterpartyConfirmedAt: Schema.optional(Schema.DateTimeUtcFromString),
|
|
178
|
+
createdAt: Schema.DateTimeUtcFromString,
|
|
179
|
+
expiresAt: Schema.DateTimeUtcFromString,
|
|
180
|
+
failureReason: Schema.optional(Schema.String),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @since 1.0.0
|
|
185
|
+
* @category Schemas
|
|
186
|
+
*/
|
|
187
|
+
export const TradeLeg = Schema.Struct({
|
|
188
|
+
role: TradeRole,
|
|
189
|
+
fromPlayerId: NimblebitConfig.PlayerIdSchema,
|
|
190
|
+
toPlayerId: NimblebitConfig.PlayerIdSchema,
|
|
191
|
+
state: LegState,
|
|
192
|
+
itemType: Schema.Enum(SyncItemType.SyncItemType),
|
|
193
|
+
item: Schema.String,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* One entry of a trade's append-only history, as shown on its timeline.
|
|
198
|
+
*
|
|
199
|
+
* @since 1.0.0
|
|
200
|
+
* @category Schemas
|
|
201
|
+
*/
|
|
202
|
+
export const LedgerEvent = Schema.Struct({
|
|
203
|
+
event: Schema.String,
|
|
204
|
+
at: Schema.DateTimeUtcFromString,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A trade with its legs and its history, for the participants only.
|
|
209
|
+
*
|
|
210
|
+
* @since 1.0.0
|
|
211
|
+
* @category Schemas
|
|
212
|
+
*/
|
|
213
|
+
export const TradeDetail = Schema.Struct({
|
|
214
|
+
trade: Trade,
|
|
215
|
+
legs: Schema.Array(TradeLeg),
|
|
216
|
+
ledger: Schema.Array(LedgerEvent),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Whether the treasury can currently act on the caller's towers, and where
|
|
221
|
+
* to go to make it so. The url is built server-side so the browser stays
|
|
222
|
+
* configuration-free.
|
|
223
|
+
*
|
|
224
|
+
* @since 1.0.0
|
|
225
|
+
* @category Schemas
|
|
226
|
+
*/
|
|
227
|
+
export const GrantStatus = Schema.Struct({
|
|
228
|
+
connected: Schema.Boolean,
|
|
229
|
+
scope: Schema.optional(Schema.String),
|
|
230
|
+
connectUrl: Schema.String,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* What a tower could put into a trade, read out of its save. Bitizens are the
|
|
235
|
+
* encoded strings the gift channel carries; costumes and pets are the strings
|
|
236
|
+
* the save stores.
|
|
237
|
+
*
|
|
238
|
+
* @since 1.0.0
|
|
239
|
+
* @category Schemas
|
|
240
|
+
*/
|
|
241
|
+
export const Inventory = Schema.Struct({
|
|
242
|
+
playerId: NimblebitConfig.PlayerIdSchema,
|
|
243
|
+
bitizens: Schema.Array(Schema.String),
|
|
244
|
+
costumes: Schema.Array(Schema.String),
|
|
245
|
+
pets: Schema.Array(Schema.String),
|
|
246
|
+
coins: Schema.Int,
|
|
247
|
+
bux: Schema.Int,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The treasury's public counters, for the home page's stat tiles. Counts
|
|
252
|
+
* only, cached server-side, and deliberately free of anything per-user.
|
|
253
|
+
*
|
|
254
|
+
* @since 1.0.0
|
|
255
|
+
* @category Schemas
|
|
256
|
+
*/
|
|
257
|
+
export const TreasuryStats = Schema.Struct({
|
|
258
|
+
activeTraders: Schema.Int,
|
|
259
|
+
settledTrades: Schema.Int,
|
|
260
|
+
itemsMoved: Schema.Int,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @since 1.0.0
|
|
265
|
+
* @category Schemas
|
|
266
|
+
*/
|
|
267
|
+
export const TradeProposal = Schema.Struct({
|
|
268
|
+
mechanism: TradeMechanism,
|
|
269
|
+
game: TradeGame,
|
|
270
|
+
proposerPlayerId: NimblebitConfig.PlayerIdSchema,
|
|
271
|
+
/** Absent for an open offer, which whoever accepts first gets. */
|
|
272
|
+
counterpartyPlayerId: Schema.optional(NimblebitConfig.PlayerIdSchema),
|
|
273
|
+
gives: ItemSpec,
|
|
274
|
+
wants: ItemSpec,
|
|
275
|
+
ttlHours: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 168 })),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Endpoints
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
const tradeId = Schema.String;
|
|
283
|
+
const playerId = NimblebitConfig.PlayerIdSchema;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* What acting on a trade can fail with: it does not exist or the caller is
|
|
287
|
+
* not a participant (`404`, and the api never confirms a trade exists to a
|
|
288
|
+
* stranger), it is not in a state the action applies to (`409`), the
|
|
289
|
+
* treasury holds no usable grant for the caller (`403`), or the world
|
|
290
|
+
* beyond - tinyburg.app, Nimblebit - did not cooperate (`503`).
|
|
291
|
+
*/
|
|
292
|
+
const TradeErrors = [
|
|
293
|
+
HttpApiError.NotFound,
|
|
294
|
+
HttpApiError.Conflict,
|
|
295
|
+
HttpApiError.Forbidden,
|
|
296
|
+
HttpApiError.ServiceUnavailable,
|
|
297
|
+
] as const;
|
|
298
|
+
|
|
299
|
+
const GetGrantStatus = HttpApiEndpoint.get("GrantStatus", "/v1/treasury/grant", {
|
|
300
|
+
success: GrantStatus,
|
|
301
|
+
}).annotate(OAuthScope.Required, Scopes.Treasury.read.grant_status.required);
|
|
302
|
+
|
|
303
|
+
const ListTrades = HttpApiEndpoint.get("ListTrades", "/v1/treasury/trades", {
|
|
304
|
+
success: Schema.Array(Trade),
|
|
305
|
+
}).annotate(OAuthScope.Required, Scopes.Treasury.read.list_trades.required);
|
|
306
|
+
|
|
307
|
+
const GetTrade = HttpApiEndpoint.get("GetTrade", "/v1/treasury/trades/:tradeId", {
|
|
308
|
+
params: { tradeId },
|
|
309
|
+
error: HttpApiError.NotFound,
|
|
310
|
+
success: TradeDetail,
|
|
311
|
+
}).annotate(OAuthScope.Required, Scopes.Treasury.read.get_trade.required);
|
|
312
|
+
|
|
313
|
+
const ProposeTrade = HttpApiEndpoint.post("ProposeTrade", "/v1/treasury/trades", {
|
|
314
|
+
payload: TradeProposal,
|
|
315
|
+
error: [HttpApiError.BadRequest, HttpApiError.Forbidden, HttpApiError.ServiceUnavailable],
|
|
316
|
+
success: Trade,
|
|
317
|
+
}).annotate(OAuthScope.Required, Scopes.Treasury.write.propose.required);
|
|
318
|
+
|
|
319
|
+
const AcceptTrade = HttpApiEndpoint.post("AcceptTrade", "/v1/treasury/trades/:tradeId/accept", {
|
|
320
|
+
params: { tradeId },
|
|
321
|
+
payload: Schema.Struct({ playerId }),
|
|
322
|
+
error: TradeErrors,
|
|
323
|
+
success: Trade,
|
|
324
|
+
}).annotate(OAuthScope.Required, Scopes.Treasury.write.accept.required);
|
|
325
|
+
|
|
326
|
+
const CancelTrade = HttpApiEndpoint.post("CancelTrade", "/v1/treasury/trades/:tradeId/cancel", {
|
|
327
|
+
params: { tradeId },
|
|
328
|
+
error: [HttpApiError.NotFound, HttpApiError.Conflict],
|
|
329
|
+
success: Trade,
|
|
330
|
+
}).annotate(OAuthScope.Required, Scopes.Treasury.write.cancel.required);
|
|
331
|
+
|
|
332
|
+
const ConfirmSplice = HttpApiEndpoint.post("ConfirmSplice", "/v1/treasury/trades/:tradeId/confirm", {
|
|
333
|
+
params: { tradeId },
|
|
334
|
+
error: [HttpApiError.NotFound, HttpApiError.Conflict],
|
|
335
|
+
success: Trade,
|
|
336
|
+
}).annotate(OAuthScope.Required, Scopes.Treasury.write.confirm_splice.required);
|
|
337
|
+
|
|
338
|
+
const ListInventory = HttpApiEndpoint.get("ListInventory", "/v1/treasury/inventory/:playerId", {
|
|
339
|
+
params: { playerId },
|
|
340
|
+
error: [HttpApiError.NotFound, HttpApiError.Forbidden, HttpApiError.ServiceUnavailable],
|
|
341
|
+
success: Inventory,
|
|
342
|
+
}).annotate(OAuthScope.Required, Scopes.Treasury.read.list_inventory.required);
|
|
343
|
+
|
|
344
|
+
const GetStats = HttpApiEndpoint.get("Stats", "/v1/treasury/stats", {
|
|
345
|
+
success: TreasuryStats,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// Groups
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Everything the treasury serves. The group annotation is the floor: an
|
|
354
|
+
* endpoint added later without its own leaf accepts only the whole
|
|
355
|
+
* `treasury` scope, so nothing defaults to a weaker permission.
|
|
356
|
+
*
|
|
357
|
+
* @since 1.0.0
|
|
358
|
+
* @category Groups
|
|
359
|
+
*/
|
|
360
|
+
export const TreasuryGroup = HttpApiGroup.make("TreasuryGroup")
|
|
361
|
+
.add(GetGrantStatus)
|
|
362
|
+
.add(ListTrades)
|
|
363
|
+
.add(GetTrade)
|
|
364
|
+
.add(ProposeTrade)
|
|
365
|
+
.add(AcceptTrade)
|
|
366
|
+
.add(CancelTrade)
|
|
367
|
+
.add(ConfirmSplice)
|
|
368
|
+
.add(ListInventory)
|
|
369
|
+
.annotate(OAuthScope.Required, NimblebitScopes.requiredBy(Scopes.Treasury))
|
|
370
|
+
.middleware(OAuthScope.RequireAccess);
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* What the treasury tells anyone at all: the stat counters the home page
|
|
374
|
+
* shows. The one group without `RequireAccess` or a scope requirement, and
|
|
375
|
+
* it must stay that way - the home page is public, so this rides
|
|
376
|
+
* tinyburg.app's proxy without a token behind it.
|
|
377
|
+
*
|
|
378
|
+
* @since 1.0.0
|
|
379
|
+
* @category Groups
|
|
380
|
+
*/
|
|
381
|
+
export const TreasuryPublicGroup = HttpApiGroup.make("TreasuryPublicGroup").add(GetStats);
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* @since 1.0.0
|
|
385
|
+
* @category Api
|
|
386
|
+
*/
|
|
387
|
+
export const Api = HttpApi.make("TreasurySdk").add(TreasuryGroup).add(TreasuryPublicGroup);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 1.0.0
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The scopes the treasury api is guarded by, as one `treasury` area.
|
|
7
|
+
*
|
|
8
|
+
* These guard the treasury's own api - proposing, accepting and watching
|
|
9
|
+
* trades - not the towers the trades move items between. Acting on a tower
|
|
10
|
+
* (depositing an item into escrow, splicing a save) happens through the
|
|
11
|
+
* tinyburg api under that game's own scopes, with a grant the player gives the
|
|
12
|
+
* treasury separately on the consent screen. Splitting the two means "let the
|
|
13
|
+
* treasury manage my trades" and "let the treasury touch my tower" are
|
|
14
|
+
* distinct questions with distinct answers.
|
|
15
|
+
*
|
|
16
|
+
* The area is built with the same `defineArea` every game's scopes are, so
|
|
17
|
+
* `Treasury.read.list_trades` is the leaf named `treasury:list_trades`, and
|
|
18
|
+
* `Treasury.read.list_trades.required` is what goes on the endpoint.
|
|
19
|
+
*
|
|
20
|
+
* @since 1.0.0
|
|
21
|
+
* @category Scopes
|
|
22
|
+
*/
|
|
23
|
+
export * as Scopes from "./Scopes.ts"
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The api the treasury serves: player-to-player trades, escrowed.
|
|
27
|
+
*
|
|
28
|
+
* Nimblebit has no hold primitive, so the treasury makes one out of the gift
|
|
29
|
+
* channel: each side's item is sent to a treasury-owned vault tower, where it
|
|
30
|
+
* sits as an *unreceived* gift - a native escrow slot - until both deposits
|
|
31
|
+
* are verified, and only then is each item forwarded to the other side. A
|
|
32
|
+
* save-splice trade instead rewrites both saves directly, which is why it
|
|
33
|
+
* requires an explicit confirmation from both parties before anything runs.
|
|
34
|
+
*
|
|
35
|
+
* Every endpoint but the public stats is bearer authenticated with a JWT
|
|
36
|
+
* access token the Tinyburg OAuth server issued for the Tinyburg api
|
|
37
|
+
* (`<site>/v1`), and guarded by a leaf of the `treasury` scope area in
|
|
38
|
+
* `Scopes`. The treasury lives behind tinyburg.app: the paths here are
|
|
39
|
+
* reached at `<site>/v1/treasury/*`, which tinyburg.app forwards verbatim,
|
|
40
|
+
* `Authorization` header included. The first-party app gets its token from
|
|
41
|
+
* tinyburg.app's `/auth/token`; any other application uses the one it was
|
|
42
|
+
* issued.
|
|
43
|
+
*
|
|
44
|
+
* A token without the endpoint's scope is refused with `403` and an
|
|
45
|
+
* `insufficient_scope` challenge. Any other `403` from a trade endpoint
|
|
46
|
+
* means the treasury holds no usable grant for the caller's towers;
|
|
47
|
+
* `GrantStatus` says so explicitly and carries the url that starts the
|
|
48
|
+
* connect flow.
|
|
49
|
+
*
|
|
50
|
+
* @since 1.0.0
|
|
51
|
+
*/
|
|
52
|
+
export * as Sdk from "./Sdk.ts"
|