@nodii/approval 0.0.1 → 0.1.4

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.
Files changed (60) hide show
  1. package/README.md +40 -2
  2. package/dist/consumer.d.ts +19 -0
  3. package/dist/consumer.d.ts.map +1 -0
  4. package/dist/consumer.js +274 -0
  5. package/dist/consumer.js.map +1 -0
  6. package/dist/deferred-actions.d.ts +34 -0
  7. package/dist/deferred-actions.d.ts.map +1 -0
  8. package/dist/deferred-actions.js +126 -0
  9. package/dist/deferred-actions.js.map +1 -0
  10. package/dist/errors.d.ts +51 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +82 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/handlers.d.ts +7 -0
  15. package/dist/handlers.d.ts.map +1 -0
  16. package/dist/handlers.js +20 -0
  17. package/dist/handlers.js.map +1 -0
  18. package/dist/index.d.ts +13 -2
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +15 -1
  21. package/dist/index.js.map +1 -0
  22. package/dist/init.d.ts +21 -0
  23. package/dist/init.d.ts.map +1 -0
  24. package/dist/init.js +139 -0
  25. package/dist/init.js.map +1 -0
  26. package/dist/kinds.d.ts +20 -0
  27. package/dist/kinds.d.ts.map +1 -0
  28. package/dist/kinds.js +72 -0
  29. package/dist/kinds.js.map +1 -0
  30. package/dist/migrations.d.ts +27 -0
  31. package/dist/migrations.d.ts.map +1 -0
  32. package/dist/migrations.js +67 -0
  33. package/dist/migrations.js.map +1 -0
  34. package/dist/policies.d.ts +21 -0
  35. package/dist/policies.d.ts.map +1 -0
  36. package/dist/policies.js +99 -0
  37. package/dist/policies.js.map +1 -0
  38. package/dist/request.d.ts +3 -0
  39. package/dist/request.d.ts.map +1 -0
  40. package/dist/request.js +356 -0
  41. package/dist/request.js.map +1 -0
  42. package/dist/task-tracking-client.d.ts +39 -0
  43. package/dist/task-tracking-client.d.ts.map +1 -0
  44. package/dist/task-tracking-client.js +100 -0
  45. package/dist/task-tracking-client.js.map +1 -0
  46. package/dist/telemetry-shim.d.ts +35 -0
  47. package/dist/telemetry-shim.d.ts.map +1 -0
  48. package/dist/telemetry-shim.js +88 -0
  49. package/dist/telemetry-shim.js.map +1 -0
  50. package/dist/types.d.ts +279 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +18 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/validation.d.ts +5 -0
  55. package/dist/validation.d.ts.map +1 -0
  56. package/dist/validation.js +40 -0
  57. package/dist/validation.js.map +1 -0
  58. package/package.json +44 -3
  59. package/src/migrations/001-approval-policies.sql +27 -0
  60. package/src/migrations/002-deferred-actions.sql +41 -0
@@ -0,0 +1,99 @@
1
+ // Per-tenant policy lookup for `<service>_approval_policies` per spec § 5.4.
2
+ //
3
+ // No caching — tenant-admins flip policy rows rarely; cache invalidation
4
+ // would be a foot-gun. Library re-reads on each `requestApproval`.
5
+ function tableName(serviceName) {
6
+ return `${serviceName}_approval_policies`;
7
+ }
8
+ export async function findPolicy(cfg, pg, tenantId, kind) {
9
+ const tbl = tableName(cfg.serviceName);
10
+ const rows = await pg.unsafe(`SELECT * FROM ${tbl} WHERE tenant_id = $1 AND kind = $2 LIMIT 1`, [tenantId, kind]);
11
+ if (!Array.isArray(rows) || rows.length === 0)
12
+ return null;
13
+ const r = rows[0];
14
+ return {
15
+ id: String(r.id),
16
+ tenant_id: String(r.tenant_id),
17
+ kind: String(r.kind),
18
+ enabled: Boolean(r.enabled),
19
+ approver_role: String(r.approver_role),
20
+ threshold_params: parseJsonb(r.threshold_params),
21
+ expiry_seconds: r.expiry_seconds == null ? null : Number(r.expiry_seconds),
22
+ created_at: String(r.created_at ?? ""),
23
+ updated_at: String(r.updated_at ?? ""),
24
+ };
25
+ }
26
+ /**
27
+ * Resolve the effective policy for a (tenant, kind) — uses the tenant row
28
+ * if present, otherwise falls through to kind defaults.
29
+ *
30
+ * Returns `{enabled: false, fallthrough: true}` when no row exists AND
31
+ * the kind's `optInDefault` is false (the spec § 5.10 opt-in-disabled
32
+ * short-circuit case).
33
+ */
34
+ export async function resolvePolicy(cfg, pg, tenantId, kindDef) {
35
+ const row = await findPolicy(cfg, pg, tenantId, kindDef.kind);
36
+ if (!row) {
37
+ if (!kindDef.optInDefault) {
38
+ return {
39
+ enabled: false,
40
+ approverRole: kindDef.approverRole,
41
+ expirySeconds: kindDef.expirySeconds ?? cfg.defaultExpirySeconds,
42
+ thresholdParams: null,
43
+ fallthrough: true,
44
+ };
45
+ }
46
+ return {
47
+ enabled: true,
48
+ approverRole: kindDef.approverRole,
49
+ expirySeconds: kindDef.expirySeconds ?? cfg.defaultExpirySeconds,
50
+ thresholdParams: null,
51
+ fallthrough: true,
52
+ };
53
+ }
54
+ return {
55
+ enabled: row.enabled,
56
+ approverRole: row.approver_role,
57
+ expirySeconds: row.expiry_seconds ?? kindDef.expirySeconds ?? cfg.defaultExpirySeconds,
58
+ thresholdParams: row.threshold_params,
59
+ fallthrough: false,
60
+ };
61
+ }
62
+ /** Upsert a policy row. Used by the per-service policy gRPC service. */
63
+ export async function upsertPolicy(cfg, pg, args) {
64
+ const tbl = tableName(cfg.serviceName);
65
+ await pg.unsafe(`INSERT INTO ${tbl}
66
+ (tenant_id, kind, enabled, approver_role, threshold_params, expiry_seconds, updated_at)
67
+ VALUES ($1, $2, $3, $4, $5, $6, now())
68
+ ON CONFLICT (tenant_id, kind) DO UPDATE
69
+ SET enabled = EXCLUDED.enabled,
70
+ approver_role = EXCLUDED.approver_role,
71
+ threshold_params = EXCLUDED.threshold_params,
72
+ expiry_seconds = EXCLUDED.expiry_seconds,
73
+ updated_at = now()`, [
74
+ args.tenantId,
75
+ args.kind,
76
+ args.enabled,
77
+ args.approverRole,
78
+ args.thresholdParams == null
79
+ ? null
80
+ : JSON.stringify(args.thresholdParams),
81
+ args.expirySeconds,
82
+ ]);
83
+ }
84
+ function parseJsonb(v) {
85
+ if (v == null)
86
+ return null;
87
+ if (typeof v === "string") {
88
+ try {
89
+ return JSON.parse(v);
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ if (typeof v === "object")
96
+ return v;
97
+ return null;
98
+ }
99
+ //# sourceMappingURL=policies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policies.js","sourceRoot":"","sources":["../src/policies.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { RequestApprovalOpts, RequestApprovalResult } from "./types";
2
+ export declare function requestApproval(opts: RequestApprovalOpts): Promise<RequestApprovalResult>;
3
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAIV,mBAAmB,EACnB,qBAAqB,EAEtB,MAAM,SAAS,CAAC;AAGjB,wBAAsB,eAAe,CACnC,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA2BhC"}
@@ -0,0 +1,356 @@
1
+ // requestApproval — the canonical 7-step sequence per spec § 5.2.
2
+ //
3
+ // [1] Validate kind against registry → UnknownApprovalKind
4
+ // [2] Wrap via @nodii/idempotency → duplicate / single-flight
5
+ // [3] Look up tenant policy → opt-in-disabled short-circuit
6
+ // [4] Check self-action exception → inline short-circuit
7
+ // [5] Persist deferred_actions row + audit
8
+ // [6] gRPC CreateTask to task-tracking → TaskCreationFailed
9
+ // [7] Persist task_id back to deferred row
10
+ //
11
+ // Per R1: real I/O against postgres + redis + task-tracking. No Noop default.
12
+ import { randomUUID, createHash } from "node:crypto";
13
+ import { wrapForSagaStep } from "@nodii/idempotency";
14
+ import { insertDeferredAction, markFailed } from "./deferred-actions";
15
+ import { HandlerNotRegistered, IdempotencyKeyRequired, UnknownApprovalKind, } from "./errors";
16
+ import { requireApproval, requireHandlers, requireKindRegistry } from "./init";
17
+ import { resolvePolicy } from "./policies";
18
+ import { emitAudit, inc, observe } from "./telemetry-shim";
19
+ import { DEFAULT_IDEMPOTENCY_TTL_SECONDS } from "./types";
20
+ export async function requestApproval(opts) {
21
+ // [1] Validate kind
22
+ if (!opts.idempotencyKey || typeof opts.idempotencyKey !== "string") {
23
+ throw new IdempotencyKeyRequired();
24
+ }
25
+ const cfg = requireApproval();
26
+ const reg = requireKindRegistry();
27
+ const def = reg.get(opts.kind);
28
+ if (!def) {
29
+ throw new UnknownApprovalKind(opts.kind, reg.keys());
30
+ }
31
+ // [2] Wrap via idempotency. Replay returns the same triple.
32
+ return wrapForSagaStep({
33
+ sagaId: opts.sagaId ??
34
+ syntheticSagaId(cfg.serviceName, def.kind, opts.idempotencyKey),
35
+ stepName: "approval_request",
36
+ serializedInput: canonicalSerialise({
37
+ kind: opts.kind,
38
+ tenantId: opts.tenantId,
39
+ requesterMembershipId: opts.requesterMembershipId,
40
+ deferredActionPayload: opts.deferredActionPayload,
41
+ }),
42
+ ttlSeconds: DEFAULT_IDEMPOTENCY_TTL_SECONDS,
43
+ handler: () => runCanonicalSequence(cfg, def, opts),
44
+ });
45
+ }
46
+ async function runCanonicalSequence(cfg, def, opts) {
47
+ const startMs = Date.now();
48
+ const pg = cfg.pgPool;
49
+ // [3] Resolve tenant policy
50
+ const policy = await resolvePolicy(cfg, pg, opts.tenantId, def);
51
+ // [3a] Opt-in default off + fallthrough → short-circuit (no task created).
52
+ // Per spec § 5.10.
53
+ if (!policy.enabled && policy.fallthrough) {
54
+ const row = await persistShortCircuitRow(cfg, def, opts, "approved", null, "opt_in_disabled_executed");
55
+ try {
56
+ await dispatchHandler(def.kind, "approved", row, {
57
+ approverKind: "audit_trail_only",
58
+ approverMembershipId: null,
59
+ approverNote: "opt_in_disabled_executed",
60
+ });
61
+ }
62
+ catch (handlerErr) {
63
+ await markHandlerFailure(cfg, opts.tenantId, row.id, def.kind, handlerErr);
64
+ throw handlerErr;
65
+ }
66
+ await pg.unsafe(`UPDATE ${cfg.serviceName}_deferred_actions SET status = 'executed', executed_at = now() WHERE id = $1`, [row.id]);
67
+ emitAudit({
68
+ tenantId: opts.tenantId,
69
+ action: `${cfg.serviceName}.approval.opt_in_disabled_executed`,
70
+ targetKind: "deferred_action",
71
+ targetId: row.id,
72
+ // Caller metadata first; lib-mandated fields last so they win.
73
+ metadata: { ...(opts.metadata ?? {}), kind: def.kind },
74
+ });
75
+ inc({
76
+ name: "approval_opt_in_disabled_total",
77
+ family: "request_handler",
78
+ labels: { service: cfg.serviceName, kind: def.kind },
79
+ });
80
+ observe({
81
+ name: "approval_decision_duration_seconds",
82
+ family: "request_handler",
83
+ labels: {
84
+ service: cfg.serviceName,
85
+ kind: def.kind,
86
+ decision: "opt_in_disabled",
87
+ },
88
+ value: (Date.now() - startMs) / 1000,
89
+ });
90
+ return {
91
+ approvalId: row.id,
92
+ taskId: null,
93
+ status: "opt_in_disabled_executed",
94
+ };
95
+ }
96
+ // [4] Self-action exception per spec § 5.9.
97
+ if (def.selfActionAllowed) {
98
+ const roles = await cfg.membershipRolesLookup(opts.tenantId, opts.requesterMembershipId);
99
+ if (roles.includes(policy.approverRole)) {
100
+ const row = await persistShortCircuitRow(cfg, def, opts, "approved", opts.requesterMembershipId, "self_action");
101
+ try {
102
+ await dispatchHandler(def.kind, "approved", row, {
103
+ approverKind: "self_action",
104
+ approverMembershipId: opts.requesterMembershipId,
105
+ approverNote: "self_action",
106
+ });
107
+ }
108
+ catch (handlerErr) {
109
+ await markHandlerFailure(cfg, opts.tenantId, row.id, def.kind, handlerErr);
110
+ throw handlerErr;
111
+ }
112
+ await pg.unsafe(`UPDATE ${cfg.serviceName}_deferred_actions SET status = 'executed', executed_at = now() WHERE id = $1`, [row.id]);
113
+ emitAudit({
114
+ tenantId: opts.tenantId,
115
+ action: `${cfg.serviceName}.approval.self_action_executed`,
116
+ targetKind: "deferred_action",
117
+ targetId: row.id,
118
+ // Caller metadata first; lib-mandated fields last so they win.
119
+ metadata: {
120
+ ...(opts.metadata ?? {}),
121
+ kind: def.kind,
122
+ requester_membership_id: opts.requesterMembershipId,
123
+ },
124
+ });
125
+ inc({
126
+ name: "approval_self_action_total",
127
+ family: "request_handler",
128
+ labels: { service: cfg.serviceName, kind: def.kind },
129
+ });
130
+ return {
131
+ approvalId: row.id,
132
+ taskId: null,
133
+ status: "self_action_executed",
134
+ };
135
+ }
136
+ }
137
+ // [5] Persist deferred-action row + audit emit (one tx)
138
+ const correlationId = randomUUID();
139
+ const row = await pg.begin(async (tx) => {
140
+ const inserted = await insertDeferredAction(cfg, tx, {
141
+ tenantId: opts.tenantId,
142
+ kind: def.kind,
143
+ requesterMembershipId: opts.requesterMembershipId,
144
+ deferredActionPayload: opts.deferredActionPayload,
145
+ idempotencyKey: opts.idempotencyKey,
146
+ metadata: opts.metadata ?? {},
147
+ auditCorrelationId: correlationId,
148
+ });
149
+ return inserted;
150
+ });
151
+ emitAudit({
152
+ tenantId: opts.tenantId,
153
+ action: `${cfg.serviceName}.approval.requested`,
154
+ targetKind: "deferred_action",
155
+ targetId: row.id,
156
+ // Caller metadata first; lib-mandated fields last so they win.
157
+ metadata: {
158
+ ...(opts.metadata ?? {}),
159
+ kind: def.kind,
160
+ requester_membership_id: opts.requesterMembershipId,
161
+ audit_correlation_id: correlationId,
162
+ },
163
+ });
164
+ // [6] gRPC CreateTask
165
+ const expirySeconds = opts.expirySeconds ?? policy.expirySeconds ?? cfg.defaultExpirySeconds;
166
+ const expiresAt = new Date(Date.now() + expirySeconds * 1000).toISOString();
167
+ const taskRequest = {
168
+ tenantId: opts.tenantId,
169
+ kind: `approval:${def.kind}`,
170
+ title: titleFor(def, opts.metadata ?? {}),
171
+ description: def.description,
172
+ assigneeRole: policy.approverRole,
173
+ // Caller metadata first; lib-mandated fields last so they win
174
+ // (task-tracking's task.metadata payload).
175
+ metadata: {
176
+ ...(opts.metadata ?? {}),
177
+ approvalId: row.id,
178
+ deferredActionId: row.id,
179
+ kind: def.kind,
180
+ requesterMembershipId: opts.requesterMembershipId,
181
+ thresholdEvalContext: policy.thresholdParams,
182
+ },
183
+ expiresAt,
184
+ idempotencyKey: opts.idempotencyKey,
185
+ sourceModule: cfg.serviceName,
186
+ actionButton: opts.actionButton ?? {
187
+ label: def.actionButtonLabel ?? "Review",
188
+ moduleKey: cfg.serviceName,
189
+ actionKey: `approval:${def.shortKind}`,
190
+ params: { approvalId: row.id },
191
+ },
192
+ };
193
+ let taskResponse;
194
+ try {
195
+ taskResponse = await cfg.taskServiceClient.createTask(taskRequest);
196
+ }
197
+ catch (err) {
198
+ emitAudit({
199
+ tenantId: opts.tenantId,
200
+ action: `${cfg.serviceName}.approval.task_creation_failed`,
201
+ targetKind: "deferred_action",
202
+ targetId: row.id,
203
+ metadata: {
204
+ kind: def.kind,
205
+ error: err instanceof Error ? err.message : String(err),
206
+ },
207
+ });
208
+ inc({
209
+ name: "approval_task_creation_failed_total",
210
+ family: "request_handler",
211
+ labels: {
212
+ service: cfg.serviceName,
213
+ kind: def.kind,
214
+ reason: classifyError(err),
215
+ },
216
+ });
217
+ throw err;
218
+ }
219
+ // [7] Persist task_id back
220
+ await pg.unsafe(`UPDATE ${cfg.serviceName}_deferred_actions SET task_id = $1 WHERE id = $2`, [taskResponse.taskId, row.id]);
221
+ inc({
222
+ name: "approval_requested_total",
223
+ family: "request_handler",
224
+ labels: {
225
+ service: cfg.serviceName,
226
+ kind: def.kind,
227
+ outcome: "task_created",
228
+ },
229
+ });
230
+ return {
231
+ approvalId: row.id,
232
+ taskId: taskResponse.taskId,
233
+ status: "task_created",
234
+ };
235
+ }
236
+ async function persistShortCircuitRow(cfg, def, opts, status, approverMembershipId, note) {
237
+ const correlationId = randomUUID();
238
+ return cfg.pgPool.begin(async (tx) => {
239
+ const row = await insertDeferredAction(cfg, tx, {
240
+ tenantId: opts.tenantId,
241
+ kind: def.kind,
242
+ requesterMembershipId: opts.requesterMembershipId,
243
+ deferredActionPayload: opts.deferredActionPayload,
244
+ idempotencyKey: opts.idempotencyKey,
245
+ metadata: opts.metadata ?? {},
246
+ auditCorrelationId: correlationId,
247
+ });
248
+ // Bump straight to approved + decided_at — handler invocation runs
249
+ // immediately after this tx commits.
250
+ await tx.unsafe(`UPDATE ${cfg.serviceName}_deferred_actions
251
+ SET status = $1, approver_membership_id = $2, approver_note = $3, decided_at = now()
252
+ WHERE id = $4`, [status, approverMembershipId, note, row.id]);
253
+ return {
254
+ ...row,
255
+ status,
256
+ approver_membership_id: approverMembershipId,
257
+ approver_note: note,
258
+ };
259
+ });
260
+ }
261
+ async function dispatchHandler(kind, status, row, decisionMeta) {
262
+ const handlers = requireHandlers();
263
+ const set = handlers.get(kind);
264
+ if (!set)
265
+ throw new HandlerNotRegistered(kind);
266
+ const ctx = {
267
+ tenantId: row.tenant_id,
268
+ deferredActionId: row.id,
269
+ deferredActionPayload: row.deferred_action_payload,
270
+ requesterMembershipId: row.requester_membership_id,
271
+ approverMembershipId: decisionMeta.approverMembershipId,
272
+ approverNote: decisionMeta.approverNote,
273
+ approverKind: decisionMeta.approverKind,
274
+ kind: row.kind,
275
+ metadata: row.metadata,
276
+ };
277
+ if (status === "approved")
278
+ await set.onApproved(ctx);
279
+ else if (status === "rejected")
280
+ await set.onRejected(ctx);
281
+ else
282
+ await set.onExpired(ctx);
283
+ }
284
+ function canonicalSerialise(obj) {
285
+ return JSON.stringify(sortKeys(obj));
286
+ }
287
+ function sortKeys(value) {
288
+ if (Array.isArray(value))
289
+ return value.map(sortKeys);
290
+ if (value && typeof value === "object") {
291
+ const out = {};
292
+ for (const k of Object.keys(value).sort()) {
293
+ out[k] = sortKeys(value[k]);
294
+ }
295
+ return out;
296
+ }
297
+ return value;
298
+ }
299
+ function syntheticSagaId(serviceName, kind, idempotencyKey) {
300
+ const h = createHash("sha256");
301
+ h.update(serviceName);
302
+ h.update("|");
303
+ h.update(kind);
304
+ h.update("|");
305
+ h.update(idempotencyKey);
306
+ return `synthetic:${serviceName}:${h.digest("hex").slice(0, 32)}`;
307
+ }
308
+ function titleFor(def, metadata) {
309
+ if (typeof metadata.title === "string" && metadata.title.length > 0) {
310
+ return metadata.title;
311
+ }
312
+ return `Approve ${def.kind}`;
313
+ }
314
+ function classifyError(err) {
315
+ if (err && typeof err === "object" && "message" in err) {
316
+ const msg = String(err.message);
317
+ if (msg.includes("UNAVAILABLE"))
318
+ return "unavailable";
319
+ if (msg.includes("FAILED_PRECONDITION"))
320
+ return "failed_precondition";
321
+ }
322
+ return "unknown";
323
+ }
324
+ /** Shared finaliser when a short-circuit (opt-in disabled / self-action)
325
+ * handler throws. Mirrors the consumer.ts apply-tx failure path —
326
+ * marks the row failed + emits the failure audit + bumps the metric.
327
+ * The caller re-throws so the request bubbles up to the originating service. */
328
+ async function markHandlerFailure(cfg, tenantId, rowId, kind, handlerErr) {
329
+ const reason = handlerErr instanceof Error ? handlerErr.message : String(handlerErr);
330
+ try {
331
+ await markFailed(cfg, cfg.pgPool, rowId, reason);
332
+ }
333
+ catch (markErr) {
334
+ // Bookkeeping itself broke — log so operators see the divergence
335
+ // (row may be left in approved/pending with no failure marking).
336
+ cfg.logger.error("approval.request.markFailed_failed", {
337
+ row_id: rowId,
338
+ kind,
339
+ original_error: reason,
340
+ markFailed_error: markErr instanceof Error ? markErr.message : String(markErr),
341
+ });
342
+ }
343
+ emitAudit({
344
+ tenantId,
345
+ action: `${cfg.serviceName}.approval.execution_failed`,
346
+ targetKind: "deferred_action",
347
+ targetId: rowId,
348
+ metadata: { kind, error: reason },
349
+ });
350
+ inc({
351
+ name: "approval_handler_failures_total",
352
+ family: "async_worker",
353
+ labels: { service: cfg.serviceName, kind, decision: "short_circuit" },
354
+ });
355
+ }
356
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import { type ChannelCredentials, type Interceptor } from "@grpc/grpc-js";
2
+ import type { CreateTaskRequest, CreateTaskResponse, TaskServiceClient } from "./types";
3
+ /** Encode/decode helpers — JSON over the wire for v0.1.0.
4
+ *
5
+ * Task-tracking's CreateTask is a one-shot RPC carrying a JSON-encoded
6
+ * envelope. Using JSON keeps the lib free of buf/protoc codegen for the
7
+ * v0.1.0 ship. Switching to protobuf-binary is a non-breaking transport
8
+ * upgrade in a future revision. */
9
+ declare function encodeRequest(req: CreateTaskRequest): Buffer;
10
+ declare function decodeResponse(buf: Buffer): CreateTaskResponse;
11
+ export declare const TASK_SERVICE_DESCRIPTOR: {
12
+ readonly path: "/nodii.task_tracking.v1.TaskService/CreateTask";
13
+ readonly encodeRequest: typeof encodeRequest;
14
+ readonly decodeResponse: typeof decodeResponse;
15
+ };
16
+ export interface TaskTrackingGrpcClientOpts {
17
+ /** URL like `task-tracking.dev.internal:50051` or `localhost:50051`. */
18
+ url: string;
19
+ /** Identifies this caller in the `x-source-module` metadata header. */
20
+ sourceModule: string;
21
+ /** Channel credentials — defaults to insecure (dev). Production uses TLS. */
22
+ credentials?: ChannelCredentials;
23
+ /** Optional grpc-js interceptors. Used by @nodii/grpc-auth's S2S envelope. */
24
+ interceptors?: Interceptor[];
25
+ /** Per-RPC deadline in ms. Default 5s. */
26
+ deadlineMs?: number;
27
+ }
28
+ /** Production-default client. Speaks real gRPC against the supplied URL. */
29
+ export declare class TaskTrackingGrpcClient implements TaskServiceClient {
30
+ private readonly client;
31
+ private readonly deadlineMs;
32
+ private readonly sourceModule;
33
+ private closed;
34
+ constructor(opts: TaskTrackingGrpcClientOpts);
35
+ createTask(req: CreateTaskRequest): Promise<CreateTaskResponse>;
36
+ close(): Promise<void>;
37
+ }
38
+ export {};
39
+ //# sourceMappingURL=task-tracking-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-tracking-client.d.ts","sourceRoot":"","sources":["../src/task-tracking-client.ts"],"names":[],"mappings":"AAmBA,OAAO,EACL,KAAK,kBAAkB,EAEvB,KAAK,WAAW,EAKjB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAIjB;;;;;mCAKmC;AACnC,iBAAS,aAAa,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAErD;AAED,iBAAS,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,CAUvD;AAED,eAAO,MAAM,uBAAuB;;;;CAI1B,CAAC;AAEX,MAAM,WAAW,0BAA0B;IACzC,wEAAwE;IACxE,GAAG,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,YAAY,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,8EAA8E;IAC9E,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,4EAA4E;AAC5E,qBAAa,sBAAuB,YAAW,iBAAiB;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,MAAM,CAAS;gBAEX,IAAI,EAAE,0BAA0B;IAkBtC,UAAU,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAoD/D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAK7B"}
@@ -0,0 +1,100 @@
1
+ // Real grpc-js client for `nodii-task-tracking.TaskService.CreateTask` per
2
+ // spec § 5.7. Pattern 1 sync.
3
+ //
4
+ // v0.1.0 status:
5
+ // - Uses @grpc/grpc-js directly with a hand-written service descriptor
6
+ // (no buf codegen) — the proto is small and stable.
7
+ // - The S2S envelope from @nodii/grpc-auth is wired as an optional client
8
+ // interceptor (caller-supplied) — full grpc-auth integration ships
9
+ // when grpc-auth's client interceptor surface is stabilised. Until
10
+ // then, services that need S2S provide their own interceptor.
11
+ //
12
+ // Integration tests spin up an in-process grpc server with the same
13
+ // service descriptor so the WIRE path is exercised — no Noop default.
14
+ //
15
+ // Per R1: this is NOT a Noop. The client speaks real gRPC. When
16
+ // `taskServiceUrl` points at an unreachable host, callers get a real
17
+ // UNAVAILABLE error — which `request.ts` maps to `TaskCreationFailed`
18
+ // per spec § 5.7.
19
+ import { Client, Metadata, credentials as grpcCredentials, status as grpcStatus, } from "@grpc/grpc-js";
20
+ import { TaskCreationFailed } from "./errors";
21
+ const SERVICE_PATH = "/nodii.task_tracking.v1.TaskService/CreateTask";
22
+ /** Encode/decode helpers — JSON over the wire for v0.1.0.
23
+ *
24
+ * Task-tracking's CreateTask is a one-shot RPC carrying a JSON-encoded
25
+ * envelope. Using JSON keeps the lib free of buf/protoc codegen for the
26
+ * v0.1.0 ship. Switching to protobuf-binary is a non-breaking transport
27
+ * upgrade in a future revision. */
28
+ function encodeRequest(req) {
29
+ return Buffer.from(JSON.stringify(req), "utf8");
30
+ }
31
+ function decodeResponse(buf) {
32
+ const parsed = JSON.parse(buf.toString("utf8"));
33
+ if (!parsed ||
34
+ typeof parsed !== "object" ||
35
+ typeof parsed.taskId !== "string") {
36
+ throw new Error(`Malformed CreateTaskResponse: ${JSON.stringify(parsed)}`);
37
+ }
38
+ return parsed;
39
+ }
40
+ export const TASK_SERVICE_DESCRIPTOR = {
41
+ path: SERVICE_PATH,
42
+ encodeRequest,
43
+ decodeResponse,
44
+ };
45
+ /** Production-default client. Speaks real gRPC against the supplied URL. */
46
+ export class TaskTrackingGrpcClient {
47
+ client;
48
+ deadlineMs;
49
+ sourceModule;
50
+ closed = false;
51
+ constructor(opts) {
52
+ if (!opts.url || typeof opts.url !== "string") {
53
+ throw new TypeError("TaskTrackingGrpcClient: url is required");
54
+ }
55
+ const channelOpts = {
56
+ interceptors: opts.interceptors ?? [],
57
+ "grpc.keepalive_time_ms": 30_000,
58
+ "grpc.keepalive_timeout_ms": 10_000,
59
+ };
60
+ this.client = new Client(opts.url, opts.credentials ?? grpcCredentials.createInsecure(), channelOpts);
61
+ this.deadlineMs = opts.deadlineMs ?? 5_000;
62
+ this.sourceModule = opts.sourceModule;
63
+ }
64
+ async createTask(req) {
65
+ if (this.closed) {
66
+ throw new TaskCreationFailed(new Error("client_closed"), "TaskTrackingGrpcClient: client closed");
67
+ }
68
+ const md = new Metadata();
69
+ md.add("x-source-module", this.sourceModule);
70
+ md.add("x-nodii-idempotency-key", req.idempotencyKey);
71
+ const deadline = new Date(Date.now() + this.deadlineMs);
72
+ return new Promise((resolve, reject) => {
73
+ this.client.makeUnaryRequest(SERVICE_PATH, encodeRequest, decodeResponse, req, md, { deadline }, (err, value) => {
74
+ if (err) {
75
+ const isUnavailable = err.code === grpcStatus.UNAVAILABLE;
76
+ const isFailedPrecondition = err.code === grpcStatus.FAILED_PRECONDITION;
77
+ const tag = isUnavailable
78
+ ? "UNAVAILABLE"
79
+ : isFailedPrecondition
80
+ ? "FAILED_PRECONDITION"
81
+ : "ERROR";
82
+ reject(new TaskCreationFailed(err, `TaskTrackingGrpcClient.createTask ${tag}: ${err.message}`));
83
+ return;
84
+ }
85
+ if (!value) {
86
+ reject(new TaskCreationFailed(null, "TaskTrackingGrpcClient.createTask: empty response"));
87
+ return;
88
+ }
89
+ resolve(value);
90
+ });
91
+ });
92
+ }
93
+ async close() {
94
+ if (this.closed)
95
+ return;
96
+ this.closed = true;
97
+ this.client.close();
98
+ }
99
+ }
100
+ //# sourceMappingURL=task-tracking-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-tracking-client.js","sourceRoot":"","sources":["../src/task-tracking-client.ts"],"names":[],"mappings":"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"}