gdc-sdk-node-ts 2.0.8 → 2.0.10
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 +4 -1
- package/dist/UserProfileIndexStore.d.ts +47 -0
- package/dist/UserProfileIndexStore.js +66 -0
- package/dist/UserProfileIndexStore.types.d.ts +16 -0
- package/dist/UserProfileIndexStore.types.js +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/node-crypto-helper.d.ts +19 -0
- package/dist/node-crypto-helper.js +27 -0
- package/dist/node-managed-wallet.d.ts +154 -0
- package/dist/node-managed-wallet.js +604 -0
- package/dist/resource-operations.d.ts +13 -3
- package/dist/resource-operations.js +10 -0
- package/dist/wallet-backed-job-manager.d.ts +19 -0
- package/dist/wallet-backed-job-manager.js +301 -0
- package/dist/wallet-backed-job-manager.types.d.ts +57 -0
- package/dist/wallet-backed-job-manager.types.js +2 -0
- package/package.json +7 -4
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
|
+
import { JobStatus } from 'gdc-common-utils-ts/models/confidential-job';
|
|
3
|
+
import { VaultMemRepository } from 'gdc-common-utils-ts/storage';
|
|
4
|
+
const JOB_COLLECTION = 'wallet-backed-jobs';
|
|
5
|
+
const RESPONSE_COLLECTION = 'wallet-backed-job-responses';
|
|
6
|
+
function clone(value) {
|
|
7
|
+
return JSON.parse(JSON.stringify(value));
|
|
8
|
+
}
|
|
9
|
+
function createRuntimeUuid() {
|
|
10
|
+
const cryptoLike = globalThis;
|
|
11
|
+
if (typeof cryptoLike.crypto?.randomUUID === 'function') {
|
|
12
|
+
return cryptoLike.crypto.randomUUID();
|
|
13
|
+
}
|
|
14
|
+
return `job-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
15
|
+
}
|
|
16
|
+
function inferFormType(content) {
|
|
17
|
+
const first = Array.isArray(content?.body?.data) ? content.body.data[0] : undefined;
|
|
18
|
+
return String(first?.type || content?.type || '').trim();
|
|
19
|
+
}
|
|
20
|
+
function updateJobShape(job, patch) {
|
|
21
|
+
return {
|
|
22
|
+
...job,
|
|
23
|
+
...patch,
|
|
24
|
+
sequence: (job.sequence || 0) + 1,
|
|
25
|
+
previousSequence: job.sequence,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Creates one backend/session job manager that stores protected job payloads in
|
|
30
|
+
* one local vault while reusing one shared wallet for protect/unprotect and
|
|
31
|
+
* transport packing.
|
|
32
|
+
*
|
|
33
|
+
* Intended for:
|
|
34
|
+
* - portal/BFF runtimes
|
|
35
|
+
* - short-lived service sessions
|
|
36
|
+
* - send-first or cache-first orchestration over one local in-memory vault
|
|
37
|
+
*/
|
|
38
|
+
export function createWalletBackedJobManager(options) {
|
|
39
|
+
const wallet = options.wallet;
|
|
40
|
+
const vaultRepository = options.vaultRepository ?? new VaultMemRepository();
|
|
41
|
+
const context = options.walletContext;
|
|
42
|
+
const localEntityId = context.profile?.profileId ?? context.runtime?.runtimeId ?? options.descriptor.profileId;
|
|
43
|
+
let isInitialized = false;
|
|
44
|
+
let listener;
|
|
45
|
+
let lastAccessToken = '';
|
|
46
|
+
async function notify() {
|
|
47
|
+
listener?.();
|
|
48
|
+
}
|
|
49
|
+
async function protectJob(job) {
|
|
50
|
+
if (wallet.protectManagedConfidentialData) {
|
|
51
|
+
return wallet.protectManagedConfidentialData(job, context);
|
|
52
|
+
}
|
|
53
|
+
return wallet.protectConfidentialData(job, localEntityId);
|
|
54
|
+
}
|
|
55
|
+
async function unprotectJob(job) {
|
|
56
|
+
if (!job?.jwe)
|
|
57
|
+
return clone(job);
|
|
58
|
+
if (wallet.unprotectManagedConfidentialData) {
|
|
59
|
+
return wallet.unprotectManagedConfidentialData(job, context);
|
|
60
|
+
}
|
|
61
|
+
return wallet.unprotectConfidentialData(job, localEntityId);
|
|
62
|
+
}
|
|
63
|
+
async function putProtectedJob(job) {
|
|
64
|
+
const protectedJob = await protectJob(job);
|
|
65
|
+
await vaultRepository.put(JOB_COLLECTION, protectedJob);
|
|
66
|
+
await notify();
|
|
67
|
+
return clone(job);
|
|
68
|
+
}
|
|
69
|
+
async function readProtectedJob(jobId) {
|
|
70
|
+
return vaultRepository.get(JOB_COLLECTION, jobId);
|
|
71
|
+
}
|
|
72
|
+
async function readUnprotectedJob(jobId) {
|
|
73
|
+
const stored = await readProtectedJob(jobId);
|
|
74
|
+
if (!stored)
|
|
75
|
+
return undefined;
|
|
76
|
+
return unprotectJob(stored);
|
|
77
|
+
}
|
|
78
|
+
async function listUnprotectedJobs(query) {
|
|
79
|
+
const stored = await vaultRepository.query(JOB_COLLECTION, query);
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const job of stored) {
|
|
82
|
+
out.push(await unprotectJob(job));
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
async function buildEnvelope(job) {
|
|
87
|
+
if (!job.content) {
|
|
88
|
+
throw new Error(`WalletBackedJobManager cannot build an envelope for job '${job.id}' without plaintext content.`);
|
|
89
|
+
}
|
|
90
|
+
if (options.recipientDidOrJwk) {
|
|
91
|
+
if (wallet.packForRecipientWithContext) {
|
|
92
|
+
return wallet.packForRecipientWithContext(job.content, options.recipientDidOrJwk, { context });
|
|
93
|
+
}
|
|
94
|
+
if (typeof options.recipientDidOrJwk === 'string' && wallet.packForRecipient) {
|
|
95
|
+
return wallet.packForRecipient(job.content, options.recipientDidOrJwk);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return JSON.stringify(job.content);
|
|
99
|
+
}
|
|
100
|
+
async function putResponse(thid, responseBody) {
|
|
101
|
+
const responseId = createRuntimeUuid();
|
|
102
|
+
const response = {
|
|
103
|
+
id: responseId,
|
|
104
|
+
thid,
|
|
105
|
+
content: clone(responseBody),
|
|
106
|
+
createdAtTimestamp: Date.now(),
|
|
107
|
+
};
|
|
108
|
+
await vaultRepository.put(RESPONSE_COLLECTION, response);
|
|
109
|
+
return responseId;
|
|
110
|
+
}
|
|
111
|
+
async function markCompleted(job, responseBody) {
|
|
112
|
+
const responseMessageId = await putResponse(String(job.thid || job.id), responseBody);
|
|
113
|
+
const updated = updateJobShape(job, {
|
|
114
|
+
status: JobStatus.COMPLETED,
|
|
115
|
+
responseMessageId,
|
|
116
|
+
errorMessage: undefined,
|
|
117
|
+
});
|
|
118
|
+
await putProtectedJob(updated);
|
|
119
|
+
return updated;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
descriptor: { ...options.descriptor },
|
|
123
|
+
get isInitialized() {
|
|
124
|
+
return isInitialized;
|
|
125
|
+
},
|
|
126
|
+
async initialize() {
|
|
127
|
+
isInitialized = true;
|
|
128
|
+
},
|
|
129
|
+
shutdown() {
|
|
130
|
+
isInitialized = false;
|
|
131
|
+
listener = undefined;
|
|
132
|
+
},
|
|
133
|
+
setListener(nextListener) {
|
|
134
|
+
listener = nextListener;
|
|
135
|
+
},
|
|
136
|
+
async createJob(content, selector) {
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
const draft = {
|
|
139
|
+
id: createRuntimeUuid(),
|
|
140
|
+
thid: String(content?.thid || '').trim() || undefined,
|
|
141
|
+
status: JobStatus.DRAFT,
|
|
142
|
+
sequence: 1,
|
|
143
|
+
createdAtTimestamp: now,
|
|
144
|
+
content: clone(content),
|
|
145
|
+
...selector,
|
|
146
|
+
};
|
|
147
|
+
await putProtectedJob(draft);
|
|
148
|
+
return clone(draft);
|
|
149
|
+
},
|
|
150
|
+
async findDraftJobByFormType(formType) {
|
|
151
|
+
const normalizedFormType = String(formType || '').trim();
|
|
152
|
+
const drafts = await listUnprotectedJobs({
|
|
153
|
+
where: [{ attribute: 'status', equals: JobStatus.DRAFT }],
|
|
154
|
+
});
|
|
155
|
+
return drafts.find((job) => inferFormType(job.content) === normalizedFormType) ?? null;
|
|
156
|
+
},
|
|
157
|
+
async createOrUpdateDraftJob(content, selector) {
|
|
158
|
+
const formType = inferFormType(content);
|
|
159
|
+
const existing = formType ? await this.findDraftJobByFormType(formType) : null;
|
|
160
|
+
if (!existing) {
|
|
161
|
+
return this.createJob(content, selector);
|
|
162
|
+
}
|
|
163
|
+
const updated = updateJobShape(existing, {
|
|
164
|
+
content: clone(content),
|
|
165
|
+
thid: String(content?.thid || '').trim() || existing.thid,
|
|
166
|
+
...selector,
|
|
167
|
+
});
|
|
168
|
+
await putProtectedJob(updated);
|
|
169
|
+
return clone(updated);
|
|
170
|
+
},
|
|
171
|
+
async sync(accessToken) {
|
|
172
|
+
lastAccessToken = accessToken;
|
|
173
|
+
const pending = await listUnprotectedJobs({
|
|
174
|
+
where: [{
|
|
175
|
+
attribute: 'status',
|
|
176
|
+
in: [JobStatus.DRAFT, JobStatus.ERROR_RETRYABLE, JobStatus.SENT],
|
|
177
|
+
}],
|
|
178
|
+
});
|
|
179
|
+
for (const job of pending) {
|
|
180
|
+
if (job.status === JobStatus.SENT && options.transport?.poll) {
|
|
181
|
+
const poll = await options.transport.poll({
|
|
182
|
+
job,
|
|
183
|
+
accessToken,
|
|
184
|
+
context,
|
|
185
|
+
});
|
|
186
|
+
if (poll.pending)
|
|
187
|
+
continue;
|
|
188
|
+
if (poll.completed && poll.responseBody !== undefined) {
|
|
189
|
+
await markCompleted(job, poll.responseBody);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const failed = updateJobShape(job, {
|
|
193
|
+
status: poll.retryable ? JobStatus.ERROR_RETRYABLE : JobStatus.FAILED,
|
|
194
|
+
errorMessage: poll.errorMessage,
|
|
195
|
+
});
|
|
196
|
+
await putProtectedJob(failed);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
await this.submitJob(job);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
async queryJobs(query) {
|
|
203
|
+
return listUnprotectedJobs(query);
|
|
204
|
+
},
|
|
205
|
+
async submitJob(job) {
|
|
206
|
+
if (!options.transport) {
|
|
207
|
+
const sent = updateJobShape(job, {
|
|
208
|
+
status: JobStatus.SENT,
|
|
209
|
+
});
|
|
210
|
+
await putProtectedJob(sent);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!lastAccessToken) {
|
|
214
|
+
throw new Error('WalletBackedJobManager.submitJob requires sync(accessToken) before transport submission.');
|
|
215
|
+
}
|
|
216
|
+
const submitting = updateJobShape(job, {
|
|
217
|
+
status: JobStatus.SUBMITTING,
|
|
218
|
+
});
|
|
219
|
+
await putProtectedJob(submitting);
|
|
220
|
+
const envelope = await buildEnvelope(submitting);
|
|
221
|
+
const submitResult = await options.transport.submit({
|
|
222
|
+
job: submitting,
|
|
223
|
+
envelope,
|
|
224
|
+
accessToken: lastAccessToken,
|
|
225
|
+
context,
|
|
226
|
+
});
|
|
227
|
+
if (submitResult.completed && submitResult.responseBody !== undefined) {
|
|
228
|
+
await markCompleted(submitting, submitResult.responseBody);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const nextStatus = submitResult.retryable
|
|
232
|
+
? JobStatus.ERROR_RETRYABLE
|
|
233
|
+
: submitResult.accepted === false
|
|
234
|
+
? JobStatus.FAILED
|
|
235
|
+
: JobStatus.SENT;
|
|
236
|
+
const updated = updateJobShape(submitting, {
|
|
237
|
+
status: nextStatus,
|
|
238
|
+
locationUrl: submitResult.locationUrl,
|
|
239
|
+
errorMessage: submitResult.errorMessage,
|
|
240
|
+
});
|
|
241
|
+
await putProtectedJob(updated);
|
|
242
|
+
if (updated.status === JobStatus.SENT && options.transport.poll) {
|
|
243
|
+
const pollResult = await options.transport.poll({
|
|
244
|
+
job: updated,
|
|
245
|
+
accessToken: lastAccessToken,
|
|
246
|
+
context,
|
|
247
|
+
});
|
|
248
|
+
if (pollResult.completed && pollResult.responseBody !== undefined) {
|
|
249
|
+
await markCompleted(updated, pollResult.responseBody);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (!pollResult.pending) {
|
|
253
|
+
const failedAfterPoll = updateJobShape(updated, {
|
|
254
|
+
status: pollResult.retryable ? JobStatus.ERROR_RETRYABLE : JobStatus.FAILED,
|
|
255
|
+
errorMessage: pollResult.errorMessage,
|
|
256
|
+
});
|
|
257
|
+
await putProtectedJob(failedAfterPoll);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
async sealJobWithToken(job, accessToken) {
|
|
262
|
+
if (!job.content)
|
|
263
|
+
return job;
|
|
264
|
+
const sealed = updateJobShape(job, {
|
|
265
|
+
content: {
|
|
266
|
+
...clone(job.content),
|
|
267
|
+
meta: {
|
|
268
|
+
...(job.content.meta || {}),
|
|
269
|
+
bearer: {
|
|
270
|
+
compact: accessToken,
|
|
271
|
+
jwt: {},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
await putProtectedJob(sealed);
|
|
277
|
+
return sealed;
|
|
278
|
+
},
|
|
279
|
+
async getJobResponseByThid(thid) {
|
|
280
|
+
const responses = await vaultRepository.query(RESPONSE_COLLECTION, {
|
|
281
|
+
where: [{ attribute: 'thid', equals: thid }],
|
|
282
|
+
orderBy: { attribute: 'createdAtTimestamp', direction: 'desc' },
|
|
283
|
+
limit: 1,
|
|
284
|
+
});
|
|
285
|
+
return responses[0]?.content ?? null;
|
|
286
|
+
},
|
|
287
|
+
generateId() {
|
|
288
|
+
return createRuntimeUuid();
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Convenience helper for the default transient backend/session mode where one
|
|
294
|
+
* in-memory vault is enough and durable cloud sync is optional.
|
|
295
|
+
*/
|
|
296
|
+
export function createWalletBackedJobManagerInMemory(options) {
|
|
297
|
+
return createWalletBackedJobManager({
|
|
298
|
+
...options,
|
|
299
|
+
vaultRepository: options.vaultRepository ?? new VaultMemRepository(),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { JobRequest } from 'gdc-common-utils-ts/models/confidential-job';
|
|
2
|
+
import type { JWK } from 'gdc-common-utils-ts/models/jwk';
|
|
3
|
+
import type { IVaultRepository } from 'gdc-common-utils-ts/storage';
|
|
4
|
+
import type { ActorProfileDescriptor, IWallet } from 'gdc-sdk-core-ts';
|
|
5
|
+
export type WalletExecutionContextLike = Readonly<{
|
|
6
|
+
profile?: Readonly<{
|
|
7
|
+
profileId: string;
|
|
8
|
+
actorType?: string;
|
|
9
|
+
actorId?: string;
|
|
10
|
+
}>;
|
|
11
|
+
runtime?: Readonly<{
|
|
12
|
+
runtimeId: string;
|
|
13
|
+
runtimeType?: string;
|
|
14
|
+
}>;
|
|
15
|
+
route?: Readonly<{
|
|
16
|
+
tenantId?: string;
|
|
17
|
+
jurisdiction?: string;
|
|
18
|
+
sector?: string;
|
|
19
|
+
}>;
|
|
20
|
+
walletId?: string;
|
|
21
|
+
}>;
|
|
22
|
+
export type WalletBackedJobSubmitResult = Readonly<{
|
|
23
|
+
accepted?: boolean;
|
|
24
|
+
completed?: boolean;
|
|
25
|
+
locationUrl?: string;
|
|
26
|
+
responseBody?: unknown;
|
|
27
|
+
errorMessage?: string;
|
|
28
|
+
retryable?: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
export type WalletBackedJobPollResult = Readonly<{
|
|
31
|
+
pending?: boolean;
|
|
32
|
+
completed?: boolean;
|
|
33
|
+
responseBody?: unknown;
|
|
34
|
+
errorMessage?: string;
|
|
35
|
+
retryable?: boolean;
|
|
36
|
+
}>;
|
|
37
|
+
export type WalletBackedJobTransport = Readonly<{
|
|
38
|
+
submit: (input: Readonly<{
|
|
39
|
+
job: JobRequest;
|
|
40
|
+
envelope: string;
|
|
41
|
+
accessToken: string;
|
|
42
|
+
context: WalletExecutionContextLike;
|
|
43
|
+
}>) => Promise<WalletBackedJobSubmitResult>;
|
|
44
|
+
poll?: (input: Readonly<{
|
|
45
|
+
job: JobRequest;
|
|
46
|
+
accessToken: string;
|
|
47
|
+
context: WalletExecutionContextLike;
|
|
48
|
+
}>) => Promise<WalletBackedJobPollResult>;
|
|
49
|
+
}>;
|
|
50
|
+
export type WalletBackedJobManagerOptions = Readonly<{
|
|
51
|
+
descriptor: ActorProfileDescriptor;
|
|
52
|
+
wallet: IWallet;
|
|
53
|
+
walletContext: WalletExecutionContextLike;
|
|
54
|
+
vaultRepository?: IVaultRepository;
|
|
55
|
+
transport?: WalletBackedJobTransport;
|
|
56
|
+
recipientDidOrJwk?: string | JWK;
|
|
57
|
+
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdc-sdk-node-ts",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.10",
|
|
4
4
|
"description": "Next-generation Node runtime package for the GDC SDK family",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Antifraud Services Inc.",
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"local:close": "PORTS=3000 bash ./scripts/local-close.sh",
|
|
17
17
|
"docker:close": "PORTS=8000 bash ./scripts/local-close.sh",
|
|
18
18
|
"test": "npm run build && node --test tests/*.test.mjs",
|
|
19
|
-
"test:e2e:
|
|
19
|
+
"test:e2e:live-controller-lifecycle": "bash ./scripts/run-live-controller-lifecycle.sh",
|
|
20
|
+
"test:e2e:live-controller-lifecycle:direct": "npm run build && RUN_LIVE_101_ORGANIZATION_CONTROLLER_LIFECYCLE_E2E=1 node --test tests/101-organization-controller-lifecycle.live.test.mjs",
|
|
21
|
+
"test:e2e:live-full-cycle": "bash ./scripts/run-live-101-full-cycle-clean.sh",
|
|
22
|
+
"test:e2e:live-full-cycle:direct": "npm run build && RUN_LIVE_101_FULL_CYCLE_E2E=1 node --test tests/101-live-full-cycle-bff-runtime.e2e.test.mjs",
|
|
20
23
|
"test:e2e:live-gw": "npm run build && RUN_LIVE_GW_E2E=1 node --test tests/live-gw-node-runtime.e2e.test.mjs",
|
|
21
24
|
"test:e2e:live-gw:professional": "npm run build && RUN_LIVE_GW_E2E=1 LIVE_GW_E2E_SUITE=professional node --test tests/live-gw-node-runtime.e2e.test.mjs",
|
|
22
25
|
"test:e2e:live-gw:individual": "npm run build && RUN_LIVE_GW_E2E=1 LIVE_GW_E2E_SUITE=individual node --test tests/live-gw-node-runtime.e2e.test.mjs",
|
|
@@ -32,8 +35,8 @@
|
|
|
32
35
|
"test:e2e:live-gw:clean": "bash ./scripts/run-live-gw-clean.sh"
|
|
33
36
|
},
|
|
34
37
|
"dependencies": {
|
|
35
|
-
"gdc-common-utils-ts": "^2.0.
|
|
36
|
-
"gdc-sdk-core-ts": "^2.0.
|
|
38
|
+
"gdc-common-utils-ts": "^2.0.15",
|
|
39
|
+
"gdc-sdk-core-ts": "^2.0.9"
|
|
37
40
|
},
|
|
38
41
|
"devDependencies": {
|
|
39
42
|
"@types/node": "^20.14.10",
|