@xkova/sdk 0.1.0-alpha.6 → 0.1.0-alpha.8
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 +174 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +59 -1
- package/dist/generated/types.d.ts +8253 -5270
- package/dist/held-approval.d.ts +41 -0
- package/dist/held-approval.js +33 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/member-bff.d.ts +46 -2
- package/dist/member-bff.js +59 -2
- package/dist/member-resources.d.ts +7 -0
- package/dist/request-signing.js +6 -0
- package/dist/resources/account-holders.d.ts +7 -0
- package/dist/resources/branding.d.ts +10 -0
- package/dist/resources/branding.js +2 -0
- package/dist/resources/escrows.d.ts +3 -0
- package/dist/resources/payment-requests.d.ts +4 -0
- package/dist/resources/tenant.d.ts +15 -0
- package/dist/resources/tenant.js +6 -1
- package/dist/resources/tokenization.d.ts +15 -0
- package/dist/resources/treasury.d.ts +404 -0
- package/dist/resources/treasury.js +115 -0
- package/dist/resources/wallets.d.ts +2 -0
- package/dist/resources/workforce-wallets.d.ts +34 -0
- package/dist/resources/workforce-wallets.js +19 -0
- package/dist/workforce-bff.d.ts +202 -0
- package/dist/workforce-bff.js +462 -0
- package/examples/postgres-checkpoint-store.mjs +116 -0
- package/examples/postgres-checkpoints.sql +9 -0
- package/llms.txt +5 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -93,6 +93,77 @@ one live successor; either key can be revoked immediately.
|
|
|
93
93
|
|
|
94
94
|
## Typed resources
|
|
95
95
|
|
|
96
|
+
### Customer workforce applications
|
|
97
|
+
|
|
98
|
+
Use `@xkova/sdk/workforce-bff` on a confidential server for an institution's
|
|
99
|
+
staff application. It is separate from member login and Console operator
|
|
100
|
+
sessions. The Console administrator first registers an exact issuer, audience,
|
|
101
|
+
JWKS endpoint, application origins/callbacks and dedicated workspace API
|
|
102
|
+
principal. They explicitly admit each immutable provider subject to an existing
|
|
103
|
+
staff membership with narrowed roles and assigned resources. Email and provider
|
|
104
|
+
groups never grant authority. A transport key cannot admit its own users.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { XKOVAWorkforceBffClient } from '@xkova/sdk/workforce-bff';
|
|
108
|
+
|
|
109
|
+
const workforce = new XKOVAWorkforceBffClient({
|
|
110
|
+
baseUrl: 'https://sandbox-api.xkova.com',
|
|
111
|
+
apiKey: process.env.XKOVA_WORKFORCE_API_KEY!,
|
|
112
|
+
workforceKey: process.env.XKOVA_WORKFORCE_KEY!,
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Use separate encrypted durable transaction/session stores implementing
|
|
117
|
+
`XKOVAWorkforceStore`. Each write must commit independently of a later callback
|
|
118
|
+
error. Serialize each reference across replicas, and make `replace` fail when
|
|
119
|
+
the record is absent. The included PostgreSQL adapter demonstrates this contract.
|
|
120
|
+
Use a distinct namespace from member sessions. Keep session material, provider
|
|
121
|
+
proofs, PKCE verifiers and refresh attempts on the server.
|
|
122
|
+
|
|
123
|
+
`beginAuthorization` returns the registered provider endpoint, audience, nonce,
|
|
124
|
+
PKCE challenge and policy. Use a short-lived HttpOnly flow cookie for its state.
|
|
125
|
+
Your BFF exchanges the real provider code and verifies its signature, issuer,
|
|
126
|
+
audience, nonce and PKCE through `completeAuthorizationCallback`. XKOVA performs
|
|
127
|
+
its own registered-provider verification before issuing the workforce session.
|
|
128
|
+
An uncertain provider exchange cannot redeem the code again. A staged proof can
|
|
129
|
+
recover the same API exchange attempt. A provider token's actual expiry bounds
|
|
130
|
+
the session; refreshing XKOVA credentials does not refresh upstream identity.
|
|
131
|
+
|
|
132
|
+
Read only `projectXKOVAWorkforceSession` into browser UI. Resolve resource access
|
|
133
|
+
per request with `resolveResources({ reference, sessionStore, browserOrigin })`.
|
|
134
|
+
The browser's Origin and session-bound CSRF proof must be checked before writes.
|
|
135
|
+
The BFF pins its registered origin, workforce application and resolved human
|
|
136
|
+
session. Roles/scopes/resources are intersected by the API, never combined with
|
|
137
|
+
the transport principal's authority. A member credential cannot supply workforce
|
|
138
|
+
authority, and workforce sessions cannot enter Console operator routes.
|
|
139
|
+
|
|
140
|
+
For `human_authorization_required`, preserve the exact request, body, origin and
|
|
141
|
+
idempotency key in a private review record. Use `inspectAction` and
|
|
142
|
+
`beginActionProof` for its exact `authorization_token`. Show the returned review
|
|
143
|
+
before starting real fresh provider authentication with the returned nonce and
|
|
144
|
+
qualified assurance policy. Validate the provider callback's own state, PKCE,
|
|
145
|
+
issuer, audience, nonce and current subject; require fresh authentication rather
|
|
146
|
+
than accepting a cached login. Submit the real token with `completeActionProof`.
|
|
147
|
+
Keep its short-lived grant server-side and pass it as `actionGrant` to
|
|
148
|
+
`resolveResources` for only the unchanged frozen request. A grant never approves
|
|
149
|
+
another request or replaces independent Safe signatures/quorum. These methods
|
|
150
|
+
do not automatically retry an action or an uncertain proof completion.
|
|
151
|
+
|
|
152
|
+
`logoutSession` durably blocks the local session before retiring it at XKOVA.
|
|
153
|
+
Retry only that same logout if the network result is uncertain. It does not
|
|
154
|
+
promise a global logout at the external provider. No request may fall back to a
|
|
155
|
+
machine principal after workforce authentication fails.
|
|
156
|
+
|
|
157
|
+
The resolved `wallets` resource supports `list`, `provision`, `register` and
|
|
158
|
+
`signingBootstrap`. Provisioning and signing need the exact one-use human grant.
|
|
159
|
+
Register only after signing the API's returned enrollment typed data. Signing
|
|
160
|
+
bootstrap requires the existing proposal ID, its frozen Safe digest and nonce.
|
|
161
|
+
Use `@xkova/browser/wallet` for the isolated browser ceremony, with a fresh
|
|
162
|
+
purpose-bound admission and the expected wallet address. Keep the BFF key,
|
|
163
|
+
provider proofs and workforce session credentials on the server. Provider
|
|
164
|
+
admission does not replace wallet possession, current signer eligibility,
|
|
165
|
+
independent-person quorum or confirmed chain settlement.
|
|
166
|
+
|
|
96
167
|
Call resource methods with typed inputs:
|
|
97
168
|
|
|
98
169
|
```ts
|
|
@@ -124,6 +195,10 @@ const balance = await xkova.accountHolders.selfBalance({
|
|
|
124
195
|
});
|
|
125
196
|
```
|
|
126
197
|
|
|
198
|
+
`selfBalance` can return a partial answer. A chain not known right now has no
|
|
199
|
+
entries in `balances` and a null `through_block` in `data_freshness`, so check
|
|
200
|
+
that before treating a missing token as zero or adding balances together.
|
|
201
|
+
|
|
127
202
|
## Register a Hosted Auth application
|
|
128
203
|
|
|
129
204
|
Hosted Auth provides an optional XKOVA-hosted sign-in flow. Register an
|
|
@@ -216,6 +291,26 @@ proofs, and manages session refresh and logout. Implement its session,
|
|
|
216
291
|
authorization-transaction, and direct proof-exchange store contracts in your
|
|
217
292
|
own database. It does not include a web framework or database adapter.
|
|
218
293
|
|
|
294
|
+
`runExclusive` is an exclusivity boundary, not an all-or-nothing SQL
|
|
295
|
+
transaction. Every awaited `put`, `replace` and `delete` must commit before
|
|
296
|
+
returning, and remain committed if later callback work throws. Wrapping the
|
|
297
|
+
whole callback in a rollback transaction can erase a staged proof after its
|
|
298
|
+
one-time provider code was consumed. The same rule protects refresh-attempt
|
|
299
|
+
checkpoints. Reconcile an uncertain write acknowledgement using the exact
|
|
300
|
+
record and attempt identity, rather than starting a new operation.
|
|
301
|
+
|
|
302
|
+
The package includes [a PostgreSQL checkpoint-store example](examples/postgres-checkpoint-store.mjs)
|
|
303
|
+
and its [application-owned table definition](examples/postgres-checkpoints.sql).
|
|
304
|
+
Install `pg` in your backend and apply the table definition through your own
|
|
305
|
+
migration process. Instantiate separate stores with namespaces such as
|
|
306
|
+
`authorization`, `sessions` and `exchange`, one shared pool and an existing
|
|
307
|
+
32-byte application encryption key. The example uses session advisory locks
|
|
308
|
+
across replicas, independent autocommit checkpoints, authenticated encryption
|
|
309
|
+
bound to namespace/reference, exact replacement without upsert, and connection
|
|
310
|
+
discard when lock release is uncertain. Do not use a transaction-pooling proxy
|
|
311
|
+
for session locks. Own key rotation, pool deadlines and expired-record cleanup
|
|
312
|
+
in your application; do not log records or encryption material.
|
|
313
|
+
|
|
219
314
|
Authorization, Account Center, and action transactions expire within ten
|
|
220
315
|
minutes of starting the request, or at the server's expiry if sooner. Late,
|
|
221
316
|
expired, or excessively long-lived responses are rejected. Keep your BFF's
|
|
@@ -246,6 +341,51 @@ To exchange a proof without a browser callback, call `establishSession` with an
|
|
|
246
341
|
`exchangeId`. Persist that ID before the first attempt and reuse it on retries.
|
|
247
342
|
The store serializes attempts and preserves the exchange and session references.
|
|
248
343
|
|
|
344
|
+
### Enroll an external identity-provider member
|
|
345
|
+
|
|
346
|
+
For a registered OIDC connection, the confidential backend can call
|
|
347
|
+
`memberAuth.enrollExternalMember({ externalSub, applicationConfigVersion,
|
|
348
|
+
idempotencyKey })`. Use the exact opaque subject from a fully verified provider
|
|
349
|
+
proof, the final read-back application configuration version, and one persisted
|
|
350
|
+
operation key. Never link by email. The API derives issuer namespace, tenant,
|
|
351
|
+
workspace and application from the registered connection and its bound
|
|
352
|
+
`member_auth` API principal. The header selector and body selector must match.
|
|
353
|
+
An existing issuer/subject membership is returned without replacing its identity,
|
|
354
|
+
status or wallet bindings. A held or closed member remains held or closed.
|
|
355
|
+
|
|
356
|
+
Enrollment creates or finds a membership, not an authenticated session or wallet.
|
|
357
|
+
The normal nonce-bound provider-proof exchange remains required. For callback
|
|
358
|
+
flows use `completeAuthorizationCallback`'s optional `ensureMemberEnrollment`
|
|
359
|
+
hook. It runs after the one-time provider proof is durably staged, with a stable
|
|
360
|
+
`idempotencyKey` preserved across retries. Verify the staged proof's signature,
|
|
361
|
+
issuer, audience, algorithm, expiry and nonce, then call `enrollExternalMember`
|
|
362
|
+
with its exact `sub`. Return that result from the hook. The helper checks that
|
|
363
|
+
the resulting authenticated session resolves to the enrolled account holder.
|
|
364
|
+
Do not put a fallible enrollment call inside `exchangeProviderCode`, before
|
|
365
|
+
the provider result has been staged. A version conflict requires reviewing the
|
|
366
|
+
changed registration and starting a new authorization flow, not blind retries.
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
const completion = await memberAuth.completeAuthorizationCallback({
|
|
370
|
+
...callbackInput,
|
|
371
|
+
ensureMemberEnrollment: async ({ proofToken, idempotencyKey }) => {
|
|
372
|
+
// This application function performs full JOSE and exact nonce validation.
|
|
373
|
+
const verified = await verifyRegisteredProviderProof(proofToken);
|
|
374
|
+
return memberAuth.enrollExternalMember({
|
|
375
|
+
externalSub: verified.sub,
|
|
376
|
+
applicationConfigVersion: registration.config_version,
|
|
377
|
+
idempotencyKey,
|
|
378
|
+
});
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The verifier and `registration` are application prerequisites in this example,
|
|
384
|
+
not SDK-provided provider validation or discovery functions. The public endpoint
|
|
385
|
+
is `POST /v1/member-auth/enrollments`; it accepts no caller-selected account
|
|
386
|
+
holder, tenant, issuer namespace, email or permissions. Enrollment alone never
|
|
387
|
+
qualifies fresh action assurance, signing authority or settlement.
|
|
388
|
+
|
|
249
389
|
### Account Center and action callbacks
|
|
250
390
|
|
|
251
391
|
Account Center accepts a same-origin `applicationReturnUri` and preserves it
|
|
@@ -464,6 +604,40 @@ try {
|
|
|
464
604
|
}
|
|
465
605
|
```
|
|
466
606
|
|
|
607
|
+
## Treasury vaults and transfer proposals
|
|
608
|
+
|
|
609
|
+
Use `client.treasury` from a trusted backend with the workspace's explicitly
|
|
610
|
+
granted treasury scopes and roles. Discover the chain and eligible assets before
|
|
611
|
+
creating a vault or proposal. An existing same-chain vault retains its real
|
|
612
|
+
owner set and threshold.
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
import { isHeldApproval } from '@xkova/sdk';
|
|
616
|
+
|
|
617
|
+
const vaults = await xkova.treasury.listWallets();
|
|
618
|
+
const proposal = await xkova.treasury.getTransfer('trf_...');
|
|
619
|
+
const counterparties = await xkova.treasury.listCounterparties({ limit: 20 });
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Vault and counterparty setup can return a governance hold. Check `isHeldApproval`
|
|
623
|
+
before reading entity fields. Keep the original request and idempotency key;
|
|
624
|
+
resume through normal policy after independently eligible approvers complete the
|
|
625
|
+
required approval. The SDK does not approve, sign or retry a held operation.
|
|
626
|
+
|
|
627
|
+
`draftTransfer` creates a proposal. `getTransfer` reads its frozen signing context
|
|
628
|
+
and current quorum. Each eligible owner produces their own signature over that
|
|
629
|
+
exact Safe transaction. `submitTransferSignature` submits an existing signature;
|
|
630
|
+
it does not create a wallet or sign on anyone's behalf. `executeTransfer` retains
|
|
631
|
+
current role, signer, nonce, quorum and policy checks. A returned relay result is
|
|
632
|
+
not settlement; read the proposal until its actual confirmed or failed outcome.
|
|
633
|
+
After a timeout, reconcile the existing proposal before creating another one.
|
|
634
|
+
|
|
635
|
+
The API key's attributed staff ID is audit metadata, not a per-human session or
|
|
636
|
+
permission to bootstrap a staff wallet. This namespace does not expose Console
|
|
637
|
+
credentials, staff-wallet bootstrap credentials, or raw private-key material.
|
|
638
|
+
The supported workforce authentication and independent signer ceremonies remain
|
|
639
|
+
separate requirements for a complete customer treasury application.
|
|
640
|
+
|
|
467
641
|
## Webhooks and generated types
|
|
468
642
|
|
|
469
643
|
```ts
|
package/dist/client.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { RampsResource } from './resources/ramps.js';
|
|
|
13
13
|
import { RwaResource } from './resources/rwa.js';
|
|
14
14
|
import { TenantResource } from './resources/tenant.js';
|
|
15
15
|
import { TokenizationResource } from './resources/tokenization.js';
|
|
16
|
+
import { TreasuryResource } from './resources/treasury.js';
|
|
16
17
|
import { TransactionsResource } from './resources/transactions.js';
|
|
17
18
|
import { WalletsResource } from './resources/wallets.js';
|
|
18
19
|
import { WebhooksResource } from './resources/webhooks.js';
|
|
@@ -47,6 +48,7 @@ export interface XKOVAMutationOptions extends XKOVARequestControl {
|
|
|
47
48
|
export declare class XKOVAClient {
|
|
48
49
|
private readonly config;
|
|
49
50
|
private readonly memberContext?;
|
|
51
|
+
private readonly workforceContext?;
|
|
50
52
|
private readonly apiBaseUrl;
|
|
51
53
|
private readonly fetchImpl;
|
|
52
54
|
private constructor();
|
|
@@ -65,6 +67,8 @@ export declare class XKOVAClient {
|
|
|
65
67
|
get rwa(): RwaResource;
|
|
66
68
|
get tenant(): TenantResource;
|
|
67
69
|
get tokenization(): TokenizationResource;
|
|
70
|
+
/** Tenant treasury operations retain server-side roles, governance and signer checks. */
|
|
71
|
+
get treasury(): TreasuryResource;
|
|
68
72
|
get transactions(): TransactionsResource;
|
|
69
73
|
get wallets(): WalletsResource;
|
|
70
74
|
get webhooks(): WebhooksResource;
|
package/dist/client.js
CHANGED
|
@@ -16,6 +16,7 @@ import { RampsResource } from './resources/ramps.js';
|
|
|
16
16
|
import { RwaResource } from './resources/rwa.js';
|
|
17
17
|
import { TenantResource } from './resources/tenant.js';
|
|
18
18
|
import { TokenizationResource } from './resources/tokenization.js';
|
|
19
|
+
import { TreasuryResource } from './resources/treasury.js';
|
|
19
20
|
import { TransactionsResource } from './resources/transactions.js';
|
|
20
21
|
import { WalletsResource } from './resources/wallets.js';
|
|
21
22
|
import { WebhooksResource } from './resources/webhooks.js';
|
|
@@ -25,11 +26,13 @@ import { WorkspacesResource } from './resources/workspaces.js';
|
|
|
25
26
|
export class XKOVAClient {
|
|
26
27
|
config;
|
|
27
28
|
memberContext;
|
|
29
|
+
workforceContext;
|
|
28
30
|
apiBaseUrl;
|
|
29
31
|
fetchImpl;
|
|
30
|
-
constructor(config, memberContext) {
|
|
32
|
+
constructor(config, memberContext, workforceContext) {
|
|
31
33
|
this.config = config;
|
|
32
34
|
this.memberContext = memberContext;
|
|
35
|
+
this.workforceContext = workforceContext;
|
|
33
36
|
if (config.timeoutMs !== undefined &&
|
|
34
37
|
(!Number.isFinite(config.timeoutMs) || config.timeoutMs <= 0)) {
|
|
35
38
|
throw new Error('XKOVAClient timeoutMs must be a positive finite number');
|
|
@@ -83,6 +86,10 @@ export class XKOVAClient {
|
|
|
83
86
|
get tokenization() {
|
|
84
87
|
return (this.namespaces.tokenization ??= new TokenizationResource(this));
|
|
85
88
|
}
|
|
89
|
+
/** Tenant treasury operations retain server-side roles, governance and signer checks. */
|
|
90
|
+
get treasury() {
|
|
91
|
+
return (this.namespaces.treasury ??= new TreasuryResource(this));
|
|
92
|
+
}
|
|
86
93
|
get transactions() {
|
|
87
94
|
return (this.namespaces.transactions ??= new TransactionsResource(this));
|
|
88
95
|
}
|
|
@@ -108,6 +115,20 @@ export class XKOVAClient {
|
|
|
108
115
|
static forMemberSession(options, context) {
|
|
109
116
|
return new XKOVAClient(options, Object.freeze({ ...context }));
|
|
110
117
|
}
|
|
118
|
+
/** @internal Created only after the workforce BFF resolves its confidential store. */
|
|
119
|
+
static forWorkforceSession(options, context) {
|
|
120
|
+
if (!/^xkova_wf_[A-Za-z0-9_-]{43}$/.test(context.workforceKey) ||
|
|
121
|
+
!/^xwfs_[A-Za-z0-9_-]{43}$/.test(context.sessionToken) ||
|
|
122
|
+
(context.humanAuthorization !== undefined &&
|
|
123
|
+
!/^[A-Za-z0-9_-]{43}$/.test(context.humanAuthorization))) {
|
|
124
|
+
throw new TypeError('Invalid workforce session material');
|
|
125
|
+
}
|
|
126
|
+
const origin = new URL(context.browserOrigin);
|
|
127
|
+
if (origin.origin !== context.browserOrigin || !isAllowedProtocol(origin)) {
|
|
128
|
+
throw new TypeError('Invalid workforce application origin');
|
|
129
|
+
}
|
|
130
|
+
return new XKOVAClient(options, undefined, Object.freeze({ ...context }));
|
|
131
|
+
}
|
|
111
132
|
/** @internal Resource transport. Use typed namespaces instead. */
|
|
112
133
|
async requestJson(options) {
|
|
113
134
|
const prepared = await this.prepareRequest(options);
|
|
@@ -211,6 +232,9 @@ export class XKOVAClient {
|
|
|
211
232
|
headers.set(name, value);
|
|
212
233
|
}
|
|
213
234
|
if (this.memberContext) {
|
|
235
|
+
if (headers.has('X-Workforce-Session') || headers.has('X-XKOVA-Workforce-Key')) {
|
|
236
|
+
throw new TypeError('Member and workforce authority cannot be combined');
|
|
237
|
+
}
|
|
214
238
|
const authority = {
|
|
215
239
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
216
240
|
'X-Member-Session-Id': this.memberContext.memberSessionId,
|
|
@@ -224,6 +248,40 @@ export class XKOVAClient {
|
|
|
224
248
|
headers.set(name, value);
|
|
225
249
|
}
|
|
226
250
|
}
|
|
251
|
+
if (this.workforceContext) {
|
|
252
|
+
if (headers.has('X-XKOVA-Human-Authorization') && !this.workforceContext.humanAuthorization) {
|
|
253
|
+
throw new TypeError('A workforce action grant must come from the confidential context');
|
|
254
|
+
}
|
|
255
|
+
for (const name of [
|
|
256
|
+
'Cookie',
|
|
257
|
+
'X-Member-Session-Id',
|
|
258
|
+
'X-XKOVA-Publishable-Key',
|
|
259
|
+
'X-Account-Holder-Id',
|
|
260
|
+
'X-End-User-Id',
|
|
261
|
+
'X-OIDC-ID-Token',
|
|
262
|
+
'X-Active-Scope',
|
|
263
|
+
'X-Workspace-Id',
|
|
264
|
+
'X-Workspace-Type',
|
|
265
|
+
]) {
|
|
266
|
+
if (headers.has(name))
|
|
267
|
+
throw new TypeError('A workforce operation cannot mix identity or workspace contexts');
|
|
268
|
+
}
|
|
269
|
+
const authority = {
|
|
270
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
271
|
+
'X-XKOVA-Workforce-Key': this.workforceContext.workforceKey,
|
|
272
|
+
'X-Workforce-Session': this.workforceContext.sessionToken,
|
|
273
|
+
Origin: this.workforceContext.browserOrigin,
|
|
274
|
+
...(this.workforceContext.humanAuthorization !== undefined && {
|
|
275
|
+
'X-XKOVA-Human-Authorization': this.workforceContext.humanAuthorization,
|
|
276
|
+
}),
|
|
277
|
+
};
|
|
278
|
+
for (const [name, value] of Object.entries(authority)) {
|
|
279
|
+
if (headers.has(name) && headers.get(name) !== value) {
|
|
280
|
+
throw new TypeError('A workforce-bound operation cannot replace its session authority');
|
|
281
|
+
}
|
|
282
|
+
headers.set(name, value);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
227
285
|
return headers;
|
|
228
286
|
}
|
|
229
287
|
requestSignal(signal) {
|