@godxjp/server-sdk 0.11.0 → 0.13.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.
- package/README.md +94 -7
- package/dist/auth.d.ts +17 -3
- package/dist/auth.js +6 -4
- package/dist/authorization.js +1 -1
- package/dist/{chunk-2N6BMNKT.js → chunk-DUFW6IQK.js} +9 -3
- package/dist/chunk-DUFW6IQK.js.map +1 -0
- package/dist/{chunk-K3AG5TCN.js → chunk-SXH6AIYT.js} +20 -2
- package/dist/chunk-SXH6AIYT.js.map +1 -0
- package/dist/hono.d.ts +6 -2
- package/dist/hono.js +27 -7
- package/dist/hono.js.map +1 -1
- package/dist/http.d.ts +8 -2
- package/dist/http.js +1 -1
- package/dist/notifications.d.ts +269 -0
- package/dist/notifications.js +418 -0
- package/dist/notifications.js.map +1 -0
- package/dist/org.js +1 -1
- package/dist/sync.js +1 -1
- package/package.json +8 -1
- package/dist/chunk-2N6BMNKT.js.map +0 -1
- package/dist/chunk-K3AG5TCN.js.map +0 -1
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GodxHttpError,
|
|
3
|
+
loadIssuer,
|
|
4
|
+
platformFetch
|
|
5
|
+
} from "./chunk-DUFW6IQK.js";
|
|
6
|
+
|
|
7
|
+
// src/notifications.ts
|
|
8
|
+
import { jwtVerify } from "jose";
|
|
9
|
+
var InvalidNoticeError = class extends Error {
|
|
10
|
+
field;
|
|
11
|
+
constructor(field, reason) {
|
|
12
|
+
super(`Invalid notice field \`${field}\`: ${reason}.`);
|
|
13
|
+
this.name = "InvalidNoticeError";
|
|
14
|
+
this.field = field;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var LOCALES = ["ja", "en", "vi"];
|
|
18
|
+
var CATEGORIES = [
|
|
19
|
+
"service",
|
|
20
|
+
"organization_invitation",
|
|
21
|
+
"access_request",
|
|
22
|
+
"service_provisioning",
|
|
23
|
+
"billing",
|
|
24
|
+
"security",
|
|
25
|
+
"operator"
|
|
26
|
+
];
|
|
27
|
+
var STYLES = ["default", "primary", "danger"];
|
|
28
|
+
var REASONS = ["none", "optional", "required"];
|
|
29
|
+
var KEYS = ["recipients", "category", "source_ref", "action_url", "title", "body", "kind", "context", "actions", "organization_id"];
|
|
30
|
+
var KIND_PATTERN = /^[a-z][a-z0-9_.-]{0,39}$/;
|
|
31
|
+
var ACTION_ID_PATTERN = /^[a-z][a-z0-9_-]{0,39}$/;
|
|
32
|
+
var LIMITS = {
|
|
33
|
+
recipients: 100,
|
|
34
|
+
recipient: 36,
|
|
35
|
+
sourceRef: 191,
|
|
36
|
+
actionUrl: 2e3,
|
|
37
|
+
title: 200,
|
|
38
|
+
body: 2e3,
|
|
39
|
+
context: 200,
|
|
40
|
+
organizationId: 64,
|
|
41
|
+
actions: 3,
|
|
42
|
+
label: 40,
|
|
43
|
+
reason: 2e3
|
|
44
|
+
};
|
|
45
|
+
function length(value) {
|
|
46
|
+
return [...value].length;
|
|
47
|
+
}
|
|
48
|
+
function blank(value) {
|
|
49
|
+
return value.replace(/^[ \t\n\r\0\x0B]+|[ \t\n\r\0\x0B]+$/g, "") === "";
|
|
50
|
+
}
|
|
51
|
+
function requiredString(value, field, max) {
|
|
52
|
+
if (typeof value !== "string" || blank(value)) throw new InvalidNoticeError(field, "is required");
|
|
53
|
+
if (length(value) > max) throw new InvalidNoticeError(field, `is longer than ${max} characters`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function isRecord(value) {
|
|
57
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
function readText(value, field) {
|
|
60
|
+
if (!isRecord(value)) throw new InvalidNoticeError(field, "must be a locale map");
|
|
61
|
+
for (const locale of Object.keys(value)) {
|
|
62
|
+
if (!LOCALES.includes(locale)) throw new InvalidNoticeError(`${field}.${locale}`, "only ja, en and vi are rendered");
|
|
63
|
+
}
|
|
64
|
+
for (const locale of LOCALES) {
|
|
65
|
+
const text = value[locale];
|
|
66
|
+
if (text !== void 0 && text !== null && typeof text !== "string") throw new InvalidNoticeError(`${field}.${locale}`, "must be a string");
|
|
67
|
+
}
|
|
68
|
+
if (typeof value["ja"] !== "string") throw new InvalidNoticeError(`${field}.ja`, "is required");
|
|
69
|
+
return { ja: value["ja"], en: value["en"] ?? null, vi: value["vi"] ?? null };
|
|
70
|
+
}
|
|
71
|
+
function textPayload(text) {
|
|
72
|
+
const out = { ja: text.ja };
|
|
73
|
+
if (typeof text.en === "string" && text.en !== "") out.en = text.en;
|
|
74
|
+
if (typeof text.vi === "string" && text.vi !== "") out.vi = text.vi;
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
function assertText(text, field, max) {
|
|
78
|
+
if (typeof text.ja !== "string" || blank(text.ja)) throw new InvalidNoticeError(`${field}.ja`, "is required");
|
|
79
|
+
for (const [locale, value] of Object.entries(textPayload(text))) {
|
|
80
|
+
if (length(value) > max) throw new InvalidNoticeError(`${field}.${locale}`, `is longer than ${max} characters`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function absoluteHttpUrl(value) {
|
|
84
|
+
if (!/^[\x21-\x7e]+$/.test(value)) return false;
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(value);
|
|
87
|
+
return (url.protocol === "http:" || url.protocol === "https:") && url.hostname !== "";
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
var Notice = class _Notice {
|
|
93
|
+
recipients;
|
|
94
|
+
sourceRef;
|
|
95
|
+
actionUrl;
|
|
96
|
+
title;
|
|
97
|
+
body;
|
|
98
|
+
category;
|
|
99
|
+
kind;
|
|
100
|
+
context;
|
|
101
|
+
actions;
|
|
102
|
+
organizationId;
|
|
103
|
+
constructor(fields) {
|
|
104
|
+
this.recipients = Object.freeze([...fields.recipients]);
|
|
105
|
+
this.sourceRef = fields.sourceRef;
|
|
106
|
+
this.actionUrl = fields.actionUrl;
|
|
107
|
+
this.title = Object.freeze({ ...fields.title });
|
|
108
|
+
this.body = Object.freeze({ ...fields.body });
|
|
109
|
+
this.category = fields.category;
|
|
110
|
+
this.kind = fields.kind;
|
|
111
|
+
this.context = fields.context === null ? null : Object.freeze({ ...fields.context });
|
|
112
|
+
this.actions = Object.freeze(fields.actions.map((a) => Object.freeze({ ...a, label: Object.freeze({ ...a.label }) })));
|
|
113
|
+
this.organizationId = fields.organizationId;
|
|
114
|
+
this.assertValid();
|
|
115
|
+
Object.freeze(this);
|
|
116
|
+
}
|
|
117
|
+
/** @throws InvalidNoticeError */
|
|
118
|
+
static create(input) {
|
|
119
|
+
return new _Notice({ ...input, category: "service", kind: null, context: null, actions: [], organizationId: null });
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Read a body in Platform's wire shape. Stricter than Platform on purpose: an unknown key or
|
|
123
|
+
* locale, which Platform would silently drop, is refused.
|
|
124
|
+
*
|
|
125
|
+
* @throws InvalidNoticeError
|
|
126
|
+
*/
|
|
127
|
+
static fromPayload(payload) {
|
|
128
|
+
if (!isRecord(payload)) throw new InvalidNoticeError("recipients", "a notice is an object");
|
|
129
|
+
for (const key of Object.keys(payload)) {
|
|
130
|
+
if (!KEYS.includes(key)) throw new InvalidNoticeError(key, "is not a field of a notice");
|
|
131
|
+
}
|
|
132
|
+
const recipients = payload["recipients"];
|
|
133
|
+
if (!Array.isArray(recipients)) throw new InvalidNoticeError("recipients", "must be a list of Platform user ids");
|
|
134
|
+
recipients.forEach((recipient, index) => {
|
|
135
|
+
if (typeof recipient !== "string") throw new InvalidNoticeError(`recipients.${index}`, "must be a string");
|
|
136
|
+
});
|
|
137
|
+
const actions = payload["actions"] ?? [];
|
|
138
|
+
if (!Array.isArray(actions)) throw new InvalidNoticeError("actions", "must be a list");
|
|
139
|
+
const kind = payload["kind"] ?? null;
|
|
140
|
+
if (kind !== null && typeof kind !== "string") throw new InvalidNoticeError("kind", "must be a string");
|
|
141
|
+
const organizationId = payload["organization_id"] ?? null;
|
|
142
|
+
if (organizationId !== null && typeof organizationId !== "string") throw new InvalidNoticeError("organization_id", "must be a string");
|
|
143
|
+
const sourceRef = payload["source_ref"];
|
|
144
|
+
if (typeof sourceRef !== "string") throw new InvalidNoticeError("source_ref", "is required");
|
|
145
|
+
const actionUrl = payload["action_url"];
|
|
146
|
+
if (typeof actionUrl !== "string") throw new InvalidNoticeError("action_url", "is required");
|
|
147
|
+
const title = readText(payload["title"], "title");
|
|
148
|
+
const body = readText(payload["body"], "body");
|
|
149
|
+
const category = payload["category"];
|
|
150
|
+
if (typeof category !== "string" || !CATEGORIES.includes(category)) {
|
|
151
|
+
throw new InvalidNoticeError("category", "must be one of Platform's notification categories");
|
|
152
|
+
}
|
|
153
|
+
const context = payload["context"] === void 0 || payload["context"] === null ? null : readText(payload["context"], "context");
|
|
154
|
+
return new _Notice({
|
|
155
|
+
recipients,
|
|
156
|
+
sourceRef,
|
|
157
|
+
actionUrl,
|
|
158
|
+
title,
|
|
159
|
+
body,
|
|
160
|
+
category,
|
|
161
|
+
kind,
|
|
162
|
+
context,
|
|
163
|
+
actions: actions.map((action, index) => readAction(action, `actions.${index}`)),
|
|
164
|
+
organizationId
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
withCategory(category) {
|
|
168
|
+
return new _Notice({ ...this, category });
|
|
169
|
+
}
|
|
170
|
+
/** One of the kinds the service declared in its manifest (`notifications`), so a person can turn it off. */
|
|
171
|
+
withKind(kind) {
|
|
172
|
+
return new _Notice({ ...this, kind });
|
|
173
|
+
}
|
|
174
|
+
/** A short line placing the notice, printed under the email body (e.g. `WEB-123 · Website`). */
|
|
175
|
+
withContext(context) {
|
|
176
|
+
return new _Notice({ ...this, context });
|
|
177
|
+
}
|
|
178
|
+
/** Replaces the quick actions (at most three). A notice with actions is never mirrored to Renraku. */
|
|
179
|
+
withActions(...actions) {
|
|
180
|
+
return new _Notice({ ...this, actions });
|
|
181
|
+
}
|
|
182
|
+
/** The CONSOLE organization id the notice is about — required for a deployment serving many organizations. */
|
|
183
|
+
forOrganization(consoleOrganizationId) {
|
|
184
|
+
return new _Notice({ ...this, organizationId: consoleOrganizationId });
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Platform's body, with only the fields that were set: an absent optional field and an empty one
|
|
188
|
+
* are not the same body to Platform's Idempotency-Key fingerprint.
|
|
189
|
+
*/
|
|
190
|
+
toPayload() {
|
|
191
|
+
const payload = {
|
|
192
|
+
recipients: [...this.recipients],
|
|
193
|
+
category: this.category,
|
|
194
|
+
source_ref: this.sourceRef,
|
|
195
|
+
action_url: this.actionUrl,
|
|
196
|
+
title: textPayload(this.title),
|
|
197
|
+
body: textPayload(this.body)
|
|
198
|
+
};
|
|
199
|
+
if (this.kind !== null) payload.kind = this.kind;
|
|
200
|
+
if (this.context !== null) payload.context = textPayload(this.context);
|
|
201
|
+
if (this.actions.length > 0) {
|
|
202
|
+
payload.actions = this.actions.map((action) => ({
|
|
203
|
+
id: action.id,
|
|
204
|
+
label: textPayload(action.label),
|
|
205
|
+
style: action.style ?? "default",
|
|
206
|
+
reason: action.reason ?? "none"
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
if (this.organizationId !== null) payload.organization_id = this.organizationId;
|
|
210
|
+
return payload;
|
|
211
|
+
}
|
|
212
|
+
assertValid() {
|
|
213
|
+
if (this.recipients.length === 0 || this.recipients.length > LIMITS.recipients) {
|
|
214
|
+
throw new InvalidNoticeError("recipients", `must list 1 to ${LIMITS.recipients} Platform user ids`);
|
|
215
|
+
}
|
|
216
|
+
this.recipients.forEach((recipient, index) => requiredString(recipient, `recipients.${index}`, LIMITS.recipient));
|
|
217
|
+
requiredString(this.sourceRef, "source_ref", LIMITS.sourceRef);
|
|
218
|
+
requiredString(this.actionUrl, "action_url", LIMITS.actionUrl);
|
|
219
|
+
if (!absoluteHttpUrl(this.actionUrl)) {
|
|
220
|
+
throw new InvalidNoticeError("action_url", "must be an absolute http(s) URL under the service base URL");
|
|
221
|
+
}
|
|
222
|
+
assertText(this.title, "title", LIMITS.title);
|
|
223
|
+
assertText(this.body, "body", LIMITS.body);
|
|
224
|
+
if (!CATEGORIES.includes(this.category)) {
|
|
225
|
+
throw new InvalidNoticeError("category", "must be one of Platform's notification categories");
|
|
226
|
+
}
|
|
227
|
+
if (this.kind !== null && !KIND_PATTERN.test(this.kind)) throw new InvalidNoticeError("kind", "must match ^[a-z][a-z0-9_.-]{0,39}$");
|
|
228
|
+
if (this.context !== null) assertText(this.context, "context", LIMITS.context);
|
|
229
|
+
if (this.actions.length > LIMITS.actions) throw new InvalidNoticeError("actions", `at most ${LIMITS.actions} quick actions`);
|
|
230
|
+
const seen = /* @__PURE__ */ new Set();
|
|
231
|
+
this.actions.forEach((action, index) => {
|
|
232
|
+
if (typeof action.id !== "string" || !ACTION_ID_PATTERN.test(action.id)) {
|
|
233
|
+
throw new InvalidNoticeError(`actions.${index}.id`, "must match ^[a-z][a-z0-9_-]{0,39}$");
|
|
234
|
+
}
|
|
235
|
+
assertText(action.label, `actions.${index}.label`, LIMITS.label);
|
|
236
|
+
if (action.style !== void 0 && !STYLES.includes(action.style)) throw new InvalidNoticeError(`actions.${index}.style`, "must be default, primary or danger");
|
|
237
|
+
if (action.reason !== void 0 && !REASONS.includes(action.reason)) throw new InvalidNoticeError(`actions.${index}.reason`, "must be none, optional or required");
|
|
238
|
+
if (seen.has(action.id)) throw new InvalidNoticeError(`actions.${index}.id`, "is not unique on the notice");
|
|
239
|
+
seen.add(action.id);
|
|
240
|
+
});
|
|
241
|
+
if (this.organizationId !== null) requiredString(this.organizationId, "organization_id", LIMITS.organizationId);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
function readAction(value, field) {
|
|
245
|
+
if (!isRecord(value)) throw new InvalidNoticeError(field, "must be an object");
|
|
246
|
+
for (const key of Object.keys(value)) {
|
|
247
|
+
if (!["id", "label", "style", "reason"].includes(key)) throw new InvalidNoticeError(field, "only id, label, style and reason are allowed");
|
|
248
|
+
}
|
|
249
|
+
const label = readText(value["label"], `${field}.label`);
|
|
250
|
+
const style = value["style"] ?? "default";
|
|
251
|
+
if (typeof style !== "string" || !STYLES.includes(style)) throw new InvalidNoticeError(`${field}.style`, "must be default, primary or danger");
|
|
252
|
+
const reason = value["reason"] ?? "none";
|
|
253
|
+
if (typeof reason !== "string" || !REASONS.includes(reason)) throw new InvalidNoticeError(`${field}.reason`, "must be none, optional or required");
|
|
254
|
+
return {
|
|
255
|
+
id: typeof value["id"] === "string" ? value["id"] : "",
|
|
256
|
+
label,
|
|
257
|
+
style,
|
|
258
|
+
reason
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
var NotificationError = class extends GodxHttpError {
|
|
262
|
+
reason;
|
|
263
|
+
requestId;
|
|
264
|
+
constructor(reason, status = 0, requestId = null, options) {
|
|
265
|
+
super(status, "", `GoDX notification failed: ${reason}.${requestId ? ` Platform request id: ${requestId}.` : ""}`);
|
|
266
|
+
this.name = "NotificationError";
|
|
267
|
+
this.reason = reason;
|
|
268
|
+
this.requestId = requestId;
|
|
269
|
+
if (options?.cause !== void 0) this.cause = options.cause;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Whether sending again may succeed. Retry only with the SAME idempotency key and notice: an
|
|
273
|
+
* unavailable answer can follow a write that happened.
|
|
274
|
+
*/
|
|
275
|
+
get retryable() {
|
|
276
|
+
return this.reason === "rate_limited" || this.reason === "unavailable";
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
var NOTIFICATIONS_PATH = "/api/sso/service/notifications";
|
|
280
|
+
function createNotificationClient(config) {
|
|
281
|
+
const configured = config.issuer !== "" && config.serviceSlug !== "" && config.serviceSecret !== "";
|
|
282
|
+
return {
|
|
283
|
+
configured,
|
|
284
|
+
async send(notice, options = {}) {
|
|
285
|
+
if (!configured) throw new NotificationError("not_configured");
|
|
286
|
+
if (options.idempotencyKey !== void 0 && (options.idempotencyKey === "" || length(options.idempotencyKey) > 191)) {
|
|
287
|
+
throw new RangeError("An Idempotency-Key is 1 to 191 characters.");
|
|
288
|
+
}
|
|
289
|
+
const headers = {
|
|
290
|
+
Accept: "application/json",
|
|
291
|
+
"Content-Type": "application/json",
|
|
292
|
+
"X-Service-Slug": config.serviceSlug,
|
|
293
|
+
"X-Service-Secret": config.serviceSecret
|
|
294
|
+
};
|
|
295
|
+
if (options.idempotencyKey !== void 0) headers["Idempotency-Key"] = options.idempotencyKey;
|
|
296
|
+
let response;
|
|
297
|
+
try {
|
|
298
|
+
response = await platformFetch(
|
|
299
|
+
new URL(NOTIFICATIONS_PATH, config.issuer),
|
|
300
|
+
{ method: "POST", headers, body: JSON.stringify(notice.toPayload()), redirect: "manual" },
|
|
301
|
+
config.http
|
|
302
|
+
);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
throw new NotificationError("unavailable", 0, null, { cause: error });
|
|
305
|
+
}
|
|
306
|
+
const requestId = response.headers.get("X-Request-Id");
|
|
307
|
+
if (response.status < 200 || response.status >= 300) {
|
|
308
|
+
const code = response.headers.get("X-Godx-Error");
|
|
309
|
+
const status = response.status;
|
|
310
|
+
throw new NotificationError(
|
|
311
|
+
status === 401 ? "unauthenticated" : code === "ORGANIZATION_NOT_SERVED" ? "organization_not_served" : status === 403 ? "forbidden" : status === 409 ? "idempotency_conflict" : status === 422 ? "invalid" : status === 429 ? "rate_limited" : status >= 500 ? "unavailable" : "unexpected_response",
|
|
312
|
+
status,
|
|
313
|
+
requestId
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
let body;
|
|
317
|
+
try {
|
|
318
|
+
body = await response.json();
|
|
319
|
+
} catch {
|
|
320
|
+
throw new NotificationError("unexpected_response", response.status, requestId);
|
|
321
|
+
}
|
|
322
|
+
return readDelivery(body, response.status, requestId);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function readDelivery(body, status, requestId) {
|
|
327
|
+
const data = isRecord(body) ? body["data"] : void 0;
|
|
328
|
+
if (!isRecord(data)) throw new NotificationError("unexpected_response", status, requestId);
|
|
329
|
+
const rows = (value, keys) => {
|
|
330
|
+
if (!Array.isArray(value)) throw new NotificationError("unexpected_response", status, requestId);
|
|
331
|
+
return value.map((row) => {
|
|
332
|
+
if (!isRecord(row) || keys.some((key) => typeof row[key] !== "string")) {
|
|
333
|
+
throw new NotificationError("unexpected_response", status, requestId);
|
|
334
|
+
}
|
|
335
|
+
return Object.fromEntries(keys.map((key) => [key, row[key]]));
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
return {
|
|
339
|
+
created: rows(data["created"], ["id", "user_id"]),
|
|
340
|
+
updated: rows(data["updated"], ["id", "user_id"]),
|
|
341
|
+
skipped: rows(data["skipped"], ["user_id", "reason"])
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
var NOTICE_ACTIONS_PATH = "/api/platform/v1/notice-actions";
|
|
345
|
+
async function handleNoticeAction(request, options) {
|
|
346
|
+
if (request.method !== "POST") return json({ error: "method_not_allowed" }, 405, { Allow: "POST" });
|
|
347
|
+
const match = /^Bearer ([A-Za-z0-9._~-]+)$/i.exec(request.headers.get("Authorization") ?? "");
|
|
348
|
+
if (!match) return json({ error: "invalid_token", error_description: "A Bearer token is required." }, 401, { "WWW-Authenticate": "Bearer" });
|
|
349
|
+
let claims;
|
|
350
|
+
try {
|
|
351
|
+
const { metadata, jwks } = await loadIssuer(options.issuer, options.http);
|
|
352
|
+
({ payload: claims } = await jwtVerify(match[1], jwks, {
|
|
353
|
+
issuer: metadata.issuer,
|
|
354
|
+
audience: options.audience,
|
|
355
|
+
algorithms: ["RS256"],
|
|
356
|
+
typ: "at+jwt",
|
|
357
|
+
requiredClaims: ["sub", "exp", "jti"],
|
|
358
|
+
// php-sdk TokenProfile leeway.
|
|
359
|
+
clockTolerance: 30
|
|
360
|
+
}));
|
|
361
|
+
} catch {
|
|
362
|
+
return json({ error: "invalid_token" }, 401, { "WWW-Authenticate": 'Bearer error="invalid_token"' });
|
|
363
|
+
}
|
|
364
|
+
const subject = claims["sub"];
|
|
365
|
+
const organization = claims["organization_context_id"];
|
|
366
|
+
if (typeof subject !== "string" || subject === "" || typeof organization !== "string" || organization === "") {
|
|
367
|
+
return json({ error: "invalid_token" }, 401, { "WWW-Authenticate": 'Bearer error="invalid_token"' });
|
|
368
|
+
}
|
|
369
|
+
const scopes = typeof claims["scope"] === "string" ? claims["scope"].split(" ").filter(Boolean) : [];
|
|
370
|
+
const clientId = typeof claims["client_id"] === "string" ? claims["client_id"] : "";
|
|
371
|
+
if (claims["prn"] !== "user" || clientId !== (options.clientId ?? "platform-notice-actions") || !scopes.includes(options.scope ?? "notices.actions:execute")) {
|
|
372
|
+
return json({ error: "forbidden" }, 403);
|
|
373
|
+
}
|
|
374
|
+
let body;
|
|
375
|
+
try {
|
|
376
|
+
body = await request.json();
|
|
377
|
+
} catch {
|
|
378
|
+
body = null;
|
|
379
|
+
}
|
|
380
|
+
const press = readPress(body, { subject, organization, clientId, scopes: Object.freeze(scopes) });
|
|
381
|
+
if (press === null) return json({ state: "invalid" }, 422);
|
|
382
|
+
const result = await options.handler(press);
|
|
383
|
+
return json(
|
|
384
|
+
{ state: result.state, ...typeof result.message === "string" && result.message !== "" ? { message: result.message } : {} },
|
|
385
|
+
result.state === "invalid" ? 422 : 200
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
function readPress(body, principal) {
|
|
389
|
+
if (!isRecord(body)) return null;
|
|
390
|
+
const sourceRef = body["source_ref"];
|
|
391
|
+
const action = body["action"];
|
|
392
|
+
const reason = body["reason"] ?? null;
|
|
393
|
+
const locale = body["locale"] ?? null;
|
|
394
|
+
const noticeId = body["notice_id"] ?? null;
|
|
395
|
+
const valid = typeof sourceRef === "string" && !blank(sourceRef) && length(sourceRef) <= LIMITS.sourceRef && typeof action === "string" && ACTION_ID_PATTERN.test(action) && (reason === null || typeof reason === "string" && length(reason) <= LIMITS.reason) && (locale === null || LOCALES.includes(locale)) && (noticeId === null || typeof noticeId === "string" && length(noticeId) <= 64);
|
|
396
|
+
if (!valid) return null;
|
|
397
|
+
return Object.freeze({
|
|
398
|
+
principal: Object.freeze(principal),
|
|
399
|
+
sourceRef,
|
|
400
|
+
action,
|
|
401
|
+
reason: typeof reason === "string" && !blank(reason) ? reason : null,
|
|
402
|
+
locale,
|
|
403
|
+
noticeId: typeof noticeId === "string" && noticeId !== "" ? noticeId : null
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
function json(body, status, headers = {}) {
|
|
407
|
+
return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...headers } });
|
|
408
|
+
}
|
|
409
|
+
export {
|
|
410
|
+
InvalidNoticeError,
|
|
411
|
+
NOTICE_ACTIONS_PATH,
|
|
412
|
+
NOTIFICATIONS_PATH,
|
|
413
|
+
Notice,
|
|
414
|
+
NotificationError,
|
|
415
|
+
createNotificationClient,
|
|
416
|
+
handleNoticeAction
|
|
417
|
+
};
|
|
418
|
+
//# sourceMappingURL=notifications.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/notifications.ts"],"sourcesContent":["/**\n * ./notifications — Notification Center, one interface for every GoDX service (CHAT-65), the\n * mirror of php-sdk's `Notifications\\Notice`, `Http\\NotificationClient` and\n * `Route::godxNoticeActions()`. Platform's contract: godx-jp/id\n * `docs/guide/downstream-service-onboarding.md` → Sending notifications.\n *\n * - Send: a {@link Notice} is the body of `POST /api/sso/service/notifications` (machine lane,\n * instance credential), checked against every rule Platform applies that can be checked here;\n * {@link createNotificationClient} sends it and answers a {@link NoticeDelivery} or throws a\n * {@link NotificationError} with one of the same `reason`s as php-sdk.\n * - Receive: {@link handleNoticeAction} answers Platform's call to\n * `POST {base_url}/api/platform/v1/notice-actions` when a person presses a quick action.\n *\n * Both SDKs run the same fixtures (`contract/notices`, byte-identical to php-sdk's\n * `tests/Fixtures/notices`).\n */\nimport { jwtVerify } from \"jose\";\nimport { GodxHttpError, type HttpOptions, loadIssuer, platformFetch } from \"./http\";\n\nexport type NoticeLocale = \"ja\" | \"en\" | \"vi\";\n\n/** Copy in the locales Platform renders: `ja` required, `en`/`vi` optional. Plain text only. */\nexport interface LocalizedText {\n readonly ja: string;\n readonly en?: string | null;\n readonly vi?: string | null;\n}\n\n/** Platform's closed categories; `service` is the one for a notice a service authors. */\nexport type NoticeCategory =\n | \"service\"\n | \"organization_invitation\"\n | \"access_request\"\n | \"service_provisioning\"\n | \"billing\"\n | \"security\"\n | \"operator\";\n\nexport type NoticeActionStyle = \"default\" | \"primary\" | \"danger\";\n\n/** Whether a press carries a reason; Platform enforces it before calling the service. */\nexport type NoticeActionReason = \"none\" | \"optional\" | \"required\";\n\n/** One quick-action button (at most three on a notice). Only a service instance may offer them. */\nexport interface NoticeActionButton {\n readonly id: string;\n readonly label: LocalizedText;\n readonly style?: NoticeActionStyle;\n readonly reason?: NoticeActionReason;\n}\n\n/** Platform's wire body, in the shape both SDKs emit: only fields that were set. */\nexport interface NoticePayload {\n recipients: string[];\n category: NoticeCategory;\n source_ref: string;\n action_url: string;\n title: Partial<Record<NoticeLocale, string>> & { ja: string };\n body: Partial<Record<NoticeLocale, string>> & { ja: string };\n kind?: string;\n context?: Partial<Record<NoticeLocale, string>> & { ja: string };\n actions?: { id: string; label: Partial<Record<NoticeLocale, string>> & { ja: string }; style: NoticeActionStyle; reason: NoticeActionReason }[];\n organization_id?: string;\n}\n\n/** A notice Platform would refuse (or silently drop part of); `field` is Laravel's dot-notation key. */\nexport class InvalidNoticeError extends Error {\n readonly field: string;\n constructor(field: string, reason: string) {\n super(`Invalid notice field \\`${field}\\`: ${reason}.`);\n this.name = \"InvalidNoticeError\";\n this.field = field;\n }\n}\n\nconst LOCALES: readonly NoticeLocale[] = [\"ja\", \"en\", \"vi\"];\nconst CATEGORIES: readonly NoticeCategory[] = [\n \"service\",\n \"organization_invitation\",\n \"access_request\",\n \"service_provisioning\",\n \"billing\",\n \"security\",\n \"operator\",\n];\nconst STYLES: readonly NoticeActionStyle[] = [\"default\", \"primary\", \"danger\"];\nconst REASONS: readonly NoticeActionReason[] = [\"none\", \"optional\", \"required\"];\nconst KEYS = [\"recipients\", \"category\", \"source_ref\", \"action_url\", \"title\", \"body\", \"kind\", \"context\", \"actions\", \"organization_id\"];\nconst KIND_PATTERN = /^[a-z][a-z0-9_.-]{0,39}$/;\nconst ACTION_ID_PATTERN = /^[a-z][a-z0-9_-]{0,39}$/;\nconst LIMITS = {\n recipients: 100,\n recipient: 36,\n sourceRef: 191,\n actionUrl: 2000,\n title: 200,\n body: 2000,\n context: 200,\n organizationId: 64,\n actions: 3,\n label: 40,\n reason: 2000,\n} as const;\n\n/** Characters as Laravel's `max` (mb_strlen) counts them: code points, not UTF-16 units. */\nfunction length(value: string): number {\n return [...value].length;\n}\n\n/** Laravel's `required`: PHP's trim(), so only ASCII whitespace counts as blank. */\nfunction blank(value: string): boolean {\n return value.replace(/^[ \\t\\n\\r\\0\\x0B]+|[ \\t\\n\\r\\0\\x0B]+$/g, \"\") === \"\";\n}\n\nfunction requiredString(value: unknown, field: string, max: number): string {\n if (typeof value !== \"string\" || blank(value)) throw new InvalidNoticeError(field, \"is required\");\n if (length(value) > max) throw new InvalidNoticeError(field, `is longer than ${max} characters`);\n return value;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** As php-sdk's LocalizedText::fromArray(): an object of ja/en/vi strings, `ja` present. */\nfunction readText(value: unknown, field: string): LocalizedText {\n if (!isRecord(value)) throw new InvalidNoticeError(field, \"must be a locale map\");\n for (const locale of Object.keys(value)) {\n if (!(LOCALES as readonly string[]).includes(locale)) throw new InvalidNoticeError(`${field}.${locale}`, \"only ja, en and vi are rendered\");\n }\n for (const locale of LOCALES) {\n const text = value[locale];\n if (text !== undefined && text !== null && typeof text !== \"string\") throw new InvalidNoticeError(`${field}.${locale}`, \"must be a string\");\n }\n if (typeof value[\"ja\"] !== \"string\") throw new InvalidNoticeError(`${field}.ja`, \"is required\");\n return { ja: value[\"ja\"], en: (value[\"en\"] as string | null | undefined) ?? null, vi: (value[\"vi\"] as string | null | undefined) ?? null };\n}\n\n/** The locale map Platform stores: `ja` always, `en`/`vi` only when given and not empty. */\nfunction textPayload(text: LocalizedText): Partial<Record<NoticeLocale, string>> & { ja: string } {\n const out: Partial<Record<NoticeLocale, string>> & { ja: string } = { ja: text.ja };\n if (typeof text.en === \"string\" && text.en !== \"\") out.en = text.en;\n if (typeof text.vi === \"string\" && text.vi !== \"\") out.vi = text.vi;\n return out;\n}\n\nfunction assertText(text: LocalizedText, field: string, max: number): void {\n if (typeof text.ja !== \"string\" || blank(text.ja)) throw new InvalidNoticeError(`${field}.ja`, \"is required\");\n for (const [locale, value] of Object.entries(textPayload(text))) {\n if (length(value) > max) throw new InvalidNoticeError(`${field}.${locale}`, `is longer than ${max} characters`);\n }\n}\n\n/** What PHP's FILTER_VALIDATE_URL + an http(s) scheme admit: printable ASCII, a scheme and a host. */\nfunction absoluteHttpUrl(value: string): boolean {\n if (!/^[\\x21-\\x7e]+$/.test(value)) return false;\n try {\n const url = new URL(value);\n return (url.protocol === \"http:\" || url.protocol === \"https:\") && url.hostname !== \"\";\n } catch {\n return false;\n }\n}\n\ninterface NoticeFields {\n readonly recipients: readonly string[];\n readonly sourceRef: string;\n readonly actionUrl: string;\n readonly title: LocalizedText;\n readonly body: LocalizedText;\n readonly category: NoticeCategory;\n readonly kind: string | null;\n readonly context: LocalizedText | null;\n readonly actions: readonly NoticeActionButton[];\n readonly organizationId: string | null;\n}\n\n/**\n * One notice, immutable: each `with…()` returns a new one, checked again.\n *\n * ```ts\n * const notice = Notice.create({\n * recipients: [approverId], // Platform user ids, 1..100\n * sourceRef: `approval:task:${taskId}`, // the dedup key: one row per ref per person\n * actionUrl: `${baseUrl}/requests/42`, // absolute, under the instance base URL\n * title: { ja: \"承認依頼\", en: \"Approval requested\" },\n * body: { ja: \"休暇申請 #42 が届きました。\" },\n * })\n * .withKind(\"approval\")\n * .withActions(\n * { id: \"approve\", label: { ja: \"承認\", en: \"Approve\" }, style: \"primary\" },\n * { id: \"reject\", label: { ja: \"却下\", en: \"Reject\" }, style: \"danger\", reason: \"required\" },\n * );\n * ```\n *\n * Every recipient must be a member of the organization with access to the service, or Platform\n * refuses the WHOLE call (422) — checked there, not here. Sending the same `sourceRef` again\n * rewrites that person's row and marks it unread, without a second email.\n */\nexport class Notice implements NoticeFields {\n readonly recipients: readonly string[];\n readonly sourceRef: string;\n readonly actionUrl: string;\n readonly title: LocalizedText;\n readonly body: LocalizedText;\n readonly category: NoticeCategory;\n readonly kind: string | null;\n readonly context: LocalizedText | null;\n readonly actions: readonly NoticeActionButton[];\n readonly organizationId: string | null;\n\n private constructor(fields: NoticeFields) {\n this.recipients = Object.freeze([...fields.recipients]);\n this.sourceRef = fields.sourceRef;\n this.actionUrl = fields.actionUrl;\n this.title = Object.freeze({ ...fields.title });\n this.body = Object.freeze({ ...fields.body });\n this.category = fields.category;\n this.kind = fields.kind;\n this.context = fields.context === null ? null : Object.freeze({ ...fields.context });\n this.actions = Object.freeze(fields.actions.map((a) => Object.freeze({ ...a, label: Object.freeze({ ...a.label }) })));\n this.organizationId = fields.organizationId;\n this.assertValid();\n Object.freeze(this);\n }\n\n /** @throws InvalidNoticeError */\n static create(input: {\n recipients: readonly string[];\n sourceRef: string;\n actionUrl: string;\n title: LocalizedText;\n body: LocalizedText;\n }): Notice {\n return new Notice({ ...input, category: \"service\", kind: null, context: null, actions: [], organizationId: null });\n }\n\n /**\n * Read a body in Platform's wire shape. Stricter than Platform on purpose: an unknown key or\n * locale, which Platform would silently drop, is refused.\n *\n * @throws InvalidNoticeError\n */\n static fromPayload(payload: unknown): Notice {\n if (!isRecord(payload)) throw new InvalidNoticeError(\"recipients\", \"a notice is an object\");\n for (const key of Object.keys(payload)) {\n if (!KEYS.includes(key)) throw new InvalidNoticeError(key, \"is not a field of a notice\");\n }\n const recipients = payload[\"recipients\"];\n if (!Array.isArray(recipients)) throw new InvalidNoticeError(\"recipients\", \"must be a list of Platform user ids\");\n recipients.forEach((recipient, index) => {\n if (typeof recipient !== \"string\") throw new InvalidNoticeError(`recipients.${index}`, \"must be a string\");\n });\n const actions = payload[\"actions\"] ?? [];\n if (!Array.isArray(actions)) throw new InvalidNoticeError(\"actions\", \"must be a list\");\n const kind = payload[\"kind\"] ?? null;\n if (kind !== null && typeof kind !== \"string\") throw new InvalidNoticeError(\"kind\", \"must be a string\");\n const organizationId = payload[\"organization_id\"] ?? null;\n if (organizationId !== null && typeof organizationId !== \"string\") throw new InvalidNoticeError(\"organization_id\", \"must be a string\");\n const sourceRef = payload[\"source_ref\"];\n if (typeof sourceRef !== \"string\") throw new InvalidNoticeError(\"source_ref\", \"is required\");\n const actionUrl = payload[\"action_url\"];\n if (typeof actionUrl !== \"string\") throw new InvalidNoticeError(\"action_url\", \"is required\");\n const title = readText(payload[\"title\"], \"title\");\n const body = readText(payload[\"body\"], \"body\");\n const category = payload[\"category\"];\n if (typeof category !== \"string\" || !(CATEGORIES as readonly string[]).includes(category)) {\n throw new InvalidNoticeError(\"category\", \"must be one of Platform's notification categories\");\n }\n const context = payload[\"context\"] === undefined || payload[\"context\"] === null ? null : readText(payload[\"context\"], \"context\");\n\n return new Notice({\n recipients: recipients as string[],\n sourceRef,\n actionUrl,\n title,\n body,\n category: category as NoticeCategory,\n kind: kind as string | null,\n context,\n actions: actions.map((action, index) => readAction(action, `actions.${index}`)),\n organizationId: organizationId as string | null,\n });\n }\n\n withCategory(category: NoticeCategory): Notice {\n return new Notice({ ...this, category });\n }\n\n /** One of the kinds the service declared in its manifest (`notifications`), so a person can turn it off. */\n withKind(kind: string | null): Notice {\n return new Notice({ ...this, kind });\n }\n\n /** A short line placing the notice, printed under the email body (e.g. `WEB-123 · Website`). */\n withContext(context: LocalizedText | null): Notice {\n return new Notice({ ...this, context });\n }\n\n /** Replaces the quick actions (at most three). A notice with actions is never mirrored to Renraku. */\n withActions(...actions: NoticeActionButton[]): Notice {\n return new Notice({ ...this, actions });\n }\n\n /** The CONSOLE organization id the notice is about — required for a deployment serving many organizations. */\n forOrganization(consoleOrganizationId: string | null): Notice {\n return new Notice({ ...this, organizationId: consoleOrganizationId });\n }\n\n /**\n * Platform's body, with only the fields that were set: an absent optional field and an empty one\n * are not the same body to Platform's Idempotency-Key fingerprint.\n */\n toPayload(): NoticePayload {\n const payload: NoticePayload = {\n recipients: [...this.recipients],\n category: this.category,\n source_ref: this.sourceRef,\n action_url: this.actionUrl,\n title: textPayload(this.title),\n body: textPayload(this.body),\n };\n if (this.kind !== null) payload.kind = this.kind;\n if (this.context !== null) payload.context = textPayload(this.context);\n if (this.actions.length > 0) {\n payload.actions = this.actions.map((action) => ({\n id: action.id,\n label: textPayload(action.label),\n style: action.style ?? \"default\",\n reason: action.reason ?? \"none\",\n }));\n }\n if (this.organizationId !== null) payload.organization_id = this.organizationId;\n return payload;\n }\n\n private assertValid(): void {\n if (this.recipients.length === 0 || this.recipients.length > LIMITS.recipients) {\n throw new InvalidNoticeError(\"recipients\", `must list 1 to ${LIMITS.recipients} Platform user ids`);\n }\n this.recipients.forEach((recipient, index) => requiredString(recipient, `recipients.${index}`, LIMITS.recipient));\n requiredString(this.sourceRef, \"source_ref\", LIMITS.sourceRef);\n requiredString(this.actionUrl, \"action_url\", LIMITS.actionUrl);\n if (!absoluteHttpUrl(this.actionUrl)) {\n throw new InvalidNoticeError(\"action_url\", \"must be an absolute http(s) URL under the service base URL\");\n }\n assertText(this.title, \"title\", LIMITS.title);\n assertText(this.body, \"body\", LIMITS.body);\n if (!(CATEGORIES as readonly string[]).includes(this.category)) {\n throw new InvalidNoticeError(\"category\", \"must be one of Platform's notification categories\");\n }\n if (this.kind !== null && !KIND_PATTERN.test(this.kind)) throw new InvalidNoticeError(\"kind\", \"must match ^[a-z][a-z0-9_.-]{0,39}$\");\n if (this.context !== null) assertText(this.context, \"context\", LIMITS.context);\n if (this.actions.length > LIMITS.actions) throw new InvalidNoticeError(\"actions\", `at most ${LIMITS.actions} quick actions`);\n const seen = new Set<string>();\n this.actions.forEach((action, index) => {\n if (typeof action.id !== \"string\" || !ACTION_ID_PATTERN.test(action.id)) {\n throw new InvalidNoticeError(`actions.${index}.id`, \"must match ^[a-z][a-z0-9_-]{0,39}$\");\n }\n assertText(action.label, `actions.${index}.label`, LIMITS.label);\n if (action.style !== undefined && !STYLES.includes(action.style)) throw new InvalidNoticeError(`actions.${index}.style`, \"must be default, primary or danger\");\n if (action.reason !== undefined && !REASONS.includes(action.reason)) throw new InvalidNoticeError(`actions.${index}.reason`, \"must be none, optional or required\");\n if (seen.has(action.id)) throw new InvalidNoticeError(`actions.${index}.id`, \"is not unique on the notice\");\n seen.add(action.id);\n });\n if (this.organizationId !== null) requiredString(this.organizationId, \"organization_id\", LIMITS.organizationId);\n }\n}\n\nfunction readAction(value: unknown, field: string): NoticeActionButton {\n if (!isRecord(value)) throw new InvalidNoticeError(field, \"must be an object\");\n for (const key of Object.keys(value)) {\n if (![\"id\", \"label\", \"style\", \"reason\"].includes(key)) throw new InvalidNoticeError(field, \"only id, label, style and reason are allowed\");\n }\n const label = readText(value[\"label\"], `${field}.label`);\n const style = value[\"style\"] ?? \"default\";\n if (typeof style !== \"string\" || !(STYLES as readonly string[]).includes(style)) throw new InvalidNoticeError(`${field}.style`, \"must be default, primary or danger\");\n const reason = value[\"reason\"] ?? \"none\";\n if (typeof reason !== \"string\" || !(REASONS as readonly string[]).includes(reason)) throw new InvalidNoticeError(`${field}.reason`, \"must be none, optional or required\");\n return {\n id: typeof value[\"id\"] === \"string\" ? value[\"id\"] : \"\",\n label,\n style: style as NoticeActionStyle,\n reason: reason as NoticeActionReason,\n };\n}\n\n/* ------------------------------------------------------------------------------------------------\n * Send\n * --------------------------------------------------------------------------------------------- */\n\n/** Why a notice was not delivered — the same strings as php-sdk's `NotificationFailure`. */\nexport type NotificationFailureReason =\n /** No instance credential was configured, so nothing was sent. */\n | \"not_configured\"\n /** 401. */\n | \"unauthenticated\"\n /** 403: `notifications_api_enabled` is off (default) or the credential is wrong — Platform answers both alike. */\n | \"forbidden\"\n /** 403 `ORGANIZATION_NOT_SERVED`: forOrganization() names one this instance does not serve. */\n | \"organization_not_served\"\n /** 409: this Idempotency-Key was already used with another body. */\n | \"idempotency_conflict\"\n /** 422: Platform refused the body — usually one recipient without access, which refuses the whole call. */\n | \"invalid\"\n | \"rate_limited\"\n /** 5xx, a timeout or a network failure. The notice may or may not have been written. */\n | \"unavailable\"\n /** A redirect, an unexpected status, or a success whose body is not Platform's shape. */\n | \"unexpected_response\";\n\n/**\n * A notice was not delivered. A {@link GodxHttpError}, so an existing catch keeps working; its\n * `body` is always empty — like php-sdk, this client never reads an error body (Platform's have\n * carried internal detail), only the `X-Godx-Error` code and `X-Request-Id`.\n */\nexport class NotificationError extends GodxHttpError {\n readonly reason: NotificationFailureReason;\n readonly requestId: string | null;\n\n constructor(reason: NotificationFailureReason, status = 0, requestId: string | null = null, options?: { cause?: unknown }) {\n super(status, \"\", `GoDX notification failed: ${reason}.${requestId ? ` Platform request id: ${requestId}.` : \"\"}`);\n this.name = \"NotificationError\";\n this.reason = reason;\n this.requestId = requestId;\n if (options?.cause !== undefined) (this as { cause?: unknown }).cause = options.cause;\n }\n\n /**\n * Whether sending again may succeed. Retry only with the SAME idempotency key and notice: an\n * unavailable answer can follow a write that happened.\n */\n get retryable(): boolean {\n return this.reason === \"rate_limited\" || this.reason === \"unavailable\";\n }\n}\n\n/** Platform's answer: rows inserted, rows rewritten (same `source_ref`), people skipped (`preference_off`). */\nexport interface NoticeDelivery {\n readonly created: readonly { id: string; user_id: string }[];\n readonly updated: readonly { id: string; user_id: string }[];\n readonly skipped: readonly { user_id: string; reason: string }[];\n}\n\nexport interface NotificationClientConfig {\n issuer: string;\n /** The INSTANCE slug (the token audience), never the client_id. */\n serviceSlug: string;\n /** The instance client_secret. */\n serviceSecret: string;\n /** Headers, deadline and fetch for every call. */\n http?: HttpOptions;\n}\n\nexport interface NotificationClient {\n /** Whether a credential is configured; without one, send() rejects with `not_configured`. */\n readonly configured: boolean;\n /**\n * `idempotencyKey` (at most 191 characters): the same key and notice replay Platform's stored\n * answer; the same key with another notice is `idempotency_conflict`.\n */\n send(notice: Notice, options?: { idempotencyKey?: string }): Promise<NoticeDelivery>;\n}\n\nexport const NOTIFICATIONS_PATH = \"/api/sso/service/notifications\";\n\n/**\n * The machine lane — the instance credential, gated by its capability `notifications_api_enabled`\n * (default off). Redirects are never followed: a credential header must not reach another origin.\n */\nexport function createNotificationClient(config: NotificationClientConfig): NotificationClient {\n const configured = config.issuer !== \"\" && config.serviceSlug !== \"\" && config.serviceSecret !== \"\";\n\n return {\n configured,\n async send(notice, options = {}) {\n if (!configured) throw new NotificationError(\"not_configured\");\n // Platform would answer 422, read as `invalid` — pointing at the notice, not the key.\n if (options.idempotencyKey !== undefined && (options.idempotencyKey === \"\" || length(options.idempotencyKey) > 191)) {\n throw new RangeError(\"An Idempotency-Key is 1 to 191 characters.\");\n }\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n \"X-Service-Slug\": config.serviceSlug,\n \"X-Service-Secret\": config.serviceSecret,\n };\n if (options.idempotencyKey !== undefined) headers[\"Idempotency-Key\"] = options.idempotencyKey;\n\n let response: Response;\n try {\n response = await platformFetch(\n new URL(NOTIFICATIONS_PATH, config.issuer),\n { method: \"POST\", headers, body: JSON.stringify(notice.toPayload()), redirect: \"manual\" },\n config.http,\n );\n } catch (error) {\n throw new NotificationError(\"unavailable\", 0, null, { cause: error });\n }\n const requestId = response.headers.get(\"X-Request-Id\");\n\n if (response.status < 200 || response.status >= 300) {\n const code = response.headers.get(\"X-Godx-Error\");\n const status = response.status;\n throw new NotificationError(\n status === 401\n ? \"unauthenticated\"\n : code === \"ORGANIZATION_NOT_SERVED\"\n ? \"organization_not_served\"\n : status === 403\n ? \"forbidden\"\n : status === 409\n ? \"idempotency_conflict\"\n : status === 422\n ? \"invalid\"\n : status === 429\n ? \"rate_limited\"\n : status >= 500\n ? \"unavailable\"\n : \"unexpected_response\",\n status,\n requestId,\n );\n }\n\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n throw new NotificationError(\"unexpected_response\", response.status, requestId);\n }\n return readDelivery(body, response.status, requestId);\n },\n };\n}\n\nfunction readDelivery(body: unknown, status: number, requestId: string | null): NoticeDelivery {\n const data = isRecord(body) ? body[\"data\"] : undefined;\n if (!isRecord(data)) throw new NotificationError(\"unexpected_response\", status, requestId);\n const rows = <K extends string>(value: unknown, keys: readonly K[]): Record<K, string>[] => {\n if (!Array.isArray(value)) throw new NotificationError(\"unexpected_response\", status, requestId);\n return value.map((row) => {\n if (!isRecord(row) || keys.some((key) => typeof row[key] !== \"string\")) {\n throw new NotificationError(\"unexpected_response\", status, requestId);\n }\n return Object.fromEntries(keys.map((key) => [key, row[key] as string])) as Record<K, string>;\n });\n };\n return {\n created: rows(data[\"created\"], [\"id\", \"user_id\"]),\n updated: rows(data[\"updated\"], [\"id\", \"user_id\"]),\n skipped: rows(data[\"skipped\"], [\"user_id\", \"reason\"]),\n };\n}\n\n/* ------------------------------------------------------------------------------------------------\n * Receive a press\n * --------------------------------------------------------------------------------------------- */\n\n/**\n * The service's answer to a press. `done` and `already_done` SETTLE the notice: Platform remembers\n * it and replays it on the next press without calling again — answer `already_done` only when the\n * effect is confirmed.\n */\nexport type NoticeActionState = \"done\" | \"already_done\" | \"denied\" | \"invalid\";\n\nexport interface NoticeActionResult {\n readonly state: NoticeActionState;\n /** One short line for the person, in the press's locale (Platform shows at most 500 characters). */\n readonly message?: string | null;\n}\n\n/** One press, after the token was verified as Platform's notice-action token for a person. */\nexport interface NoticeAction {\n /** Who pressed (`sub`) and in which organization (`organization_context_id`, the console id) — from the token, never the body. */\n readonly principal: { readonly subject: string; readonly organization: string; readonly clientId: string; readonly scopes: readonly string[] };\n readonly sourceRef: string;\n /** The button id. */\n readonly action: string;\n /** Null when none was written. */\n readonly reason: string | null;\n /** The language to answer `message` in. */\n readonly locale: NoticeLocale | null;\n readonly noticeId: string | null;\n}\n\n/**\n * Platform decided NOTHING: it checked only that the person may still open the service and that the\n * notice offered this button. The handler, as `press.principal` and under the service's own rules,\n * finds the subject by `sourceRef` WITHIN `press.principal.organization`, answers `invalid` for an\n * action it does not offer, re-checks the person's right (`denied`), and makes the change\n * atomically. A thrown error is a 500, which the person sees as a failure; nothing is settled.\n */\nexport type NoticeActionHandler = (press: NoticeAction) => NoticeActionResult | Promise<NoticeActionResult>;\n\nexport const NOTICE_ACTIONS_PATH = \"/api/platform/v1/notice-actions\";\n\nexport interface NoticeActionOptions {\n issuer: string;\n /** Your instance slug — the token's `aud`. */\n audience: string;\n handler: NoticeActionHandler;\n /** Default `platform-notice-actions`. */\n clientId?: string;\n /** Default `notices.actions:execute`. */\n scope?: string;\n /** Headers, deadline and fetch for discovery and JWKS. */\n http?: HttpOptions;\n}\n\n/**\n * Answers Platform's `POST {base_url}/api/platform/v1/notice-actions` (Fetch API: Workers, Node 18+;\n * Hono: `app.post(NOTICE_ACTIONS_PATH, (c) => handleNoticeAction(c.req.raw, options))`).\n *\n * The token is verified like php-sdk's `AuthenticateResource` does (RS256, `typ` at+jwt, exact\n * issuer from discovery, `aud`, `exp`; else 401) — not with {@link verifyAccessToken}, because\n * Platform mints this token without `organization_id`. Then it must be a PERSON's (`prn` user)\n * from `clientId` carrying `scope`, else `403 {\"error\":\"forbidden\"}`; a malformed body is\n * `422 {\"state\":\"invalid\"}`. The handler's answer is `{state, message?}`: 200, or 422 for `invalid`.\n */\nexport async function handleNoticeAction(request: Request, options: NoticeActionOptions): Promise<Response> {\n if (request.method !== \"POST\") return json({ error: \"method_not_allowed\" }, 405, { Allow: \"POST\" });\n const match = /^Bearer ([A-Za-z0-9._~-]+)$/i.exec(request.headers.get(\"Authorization\") ?? \"\");\n if (!match) return json({ error: \"invalid_token\", error_description: \"A Bearer token is required.\" }, 401, { \"WWW-Authenticate\": \"Bearer\" });\n\n let claims: Record<string, unknown>;\n try {\n const { metadata, jwks } = await loadIssuer(options.issuer, options.http);\n ({ payload: claims } = await jwtVerify(match[1]!, jwks, {\n issuer: metadata.issuer,\n audience: options.audience,\n algorithms: [\"RS256\"],\n typ: \"at+jwt\",\n requiredClaims: [\"sub\", \"exp\", \"jti\"],\n // php-sdk TokenProfile leeway.\n clockTolerance: 30,\n }));\n } catch {\n return json({ error: \"invalid_token\" }, 401, { \"WWW-Authenticate\": 'Bearer error=\"invalid_token\"' });\n }\n const subject = claims[\"sub\"];\n const organization = claims[\"organization_context_id\"];\n if (typeof subject !== \"string\" || subject === \"\" || typeof organization !== \"string\" || organization === \"\") {\n return json({ error: \"invalid_token\" }, 401, { \"WWW-Authenticate\": 'Bearer error=\"invalid_token\"' });\n }\n const scopes = typeof claims[\"scope\"] === \"string\" ? claims[\"scope\"].split(\" \").filter(Boolean) : [];\n const clientId = typeof claims[\"client_id\"] === \"string\" ? claims[\"client_id\"] : \"\";\n if (claims[\"prn\"] !== \"user\" || clientId !== (options.clientId ?? \"platform-notice-actions\") || !scopes.includes(options.scope ?? \"notices.actions:execute\")) {\n return json({ error: \"forbidden\" }, 403);\n }\n\n let body: unknown;\n try {\n body = await request.json();\n } catch {\n body = null;\n }\n const press = readPress(body, { subject, organization, clientId, scopes: Object.freeze(scopes) });\n if (press === null) return json({ state: \"invalid\" }, 422);\n\n const result = await options.handler(press);\n return json(\n { state: result.state, ...(typeof result.message === \"string\" && result.message !== \"\" ? { message: result.message } : {}) },\n result.state === \"invalid\" ? 422 : 200,\n );\n}\n\nfunction readPress(body: unknown, principal: NoticeAction[\"principal\"]): NoticeAction | null {\n if (!isRecord(body)) return null;\n const sourceRef = body[\"source_ref\"];\n const action = body[\"action\"];\n const reason = body[\"reason\"] ?? null;\n const locale = body[\"locale\"] ?? null;\n const noticeId = body[\"notice_id\"] ?? null;\n const valid =\n typeof sourceRef === \"string\" && !blank(sourceRef) && length(sourceRef) <= LIMITS.sourceRef &&\n typeof action === \"string\" && ACTION_ID_PATTERN.test(action) &&\n (reason === null || (typeof reason === \"string\" && length(reason) <= LIMITS.reason)) &&\n (locale === null || (LOCALES as readonly unknown[]).includes(locale)) &&\n (noticeId === null || (typeof noticeId === \"string\" && length(noticeId) <= 64));\n if (!valid) return null;\n return Object.freeze({\n principal: Object.freeze(principal),\n sourceRef: sourceRef as string,\n action: action as string,\n reason: typeof reason === \"string\" && !blank(reason) ? reason : null,\n locale: locale as NoticeLocale | null,\n noticeId: typeof noticeId === \"string\" && noticeId !== \"\" ? noticeId : null,\n });\n}\n\nfunction json(body: unknown, status: number, headers: Record<string, string> = {}): Response {\n return new Response(JSON.stringify(body), { status, headers: { \"Content-Type\": \"application/json\", ...headers } });\n}\n"],"mappings":"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","names":[]}
|
package/dist/org.js
CHANGED
package/dist/sync.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/server-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "GoDX server-side SDK (Workers/Node) — holds service credentials; for browser code use @godxjp/web-sdk. Counterpart of godx-jp/php-sdk.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -63,6 +63,13 @@
|
|
|
63
63
|
"browser": "./dist/browser-guard.js",
|
|
64
64
|
"default": "./dist/producer.js"
|
|
65
65
|
},
|
|
66
|
+
"./notifications": {
|
|
67
|
+
"types": "./dist/notifications.d.ts",
|
|
68
|
+
"workerd": "./dist/notifications.js",
|
|
69
|
+
"worker": "./dist/notifications.js",
|
|
70
|
+
"browser": "./dist/browser-guard.js",
|
|
71
|
+
"default": "./dist/notifications.js"
|
|
72
|
+
},
|
|
66
73
|
"./logger": {
|
|
67
74
|
"types": "./dist/logger.d.ts",
|
|
68
75
|
"workerd": "./dist/logger.js",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/http.ts"],"sourcesContent":["/**\n * ./http — endpoint contract from OIDC discovery, the SSO client, and the\n * server-to-server service API. Runtime-agnostic: fetch + WebCrypto only.\n * Mirrors php-sdk's Http module boundary.\n */\nimport { createRemoteJWKSet, customFetch } from \"jose\";\n\n/**\n * Per-call knobs for every outbound request this SDK makes (godx-jp/server-sdk#17, from the\n * pages.godx.jp incident of 2026-10-04 where a Platform call hung 25–180 s with no request id\n * to find it by). Pass them on the client/config (`http`) or per call where a function takes\n * them; defaults are exactly the old behaviour.\n */\nexport interface HttpOptions {\n /**\n * Extra headers on every request — `X-Request-Id` is the one that makes a hang findable in\n * Platform's log. A function is called per request (mint a fresh id each time); a header the\n * SDK itself sets (Authorization, Content-Type, X-Service-*) always wins.\n */\n headers?: Record<string, string> | (() => Record<string, string>);\n /** Abort a request after this many milliseconds and throw {@link GodxTimeoutError}. Unset = the runtime's default (often none). */\n timeoutMs?: number;\n /** The fetch to use (tests, custom dispatchers). Default `globalThis.fetch`, read at call time. */\n fetch?: typeof fetch;\n}\n\n/** The request did not answer within `timeoutMs`; `url` and `timeoutMs` say which and how long. */\nexport class GodxTimeoutError extends Error {\n readonly url: string;\n readonly timeoutMs: number;\n constructor(url: string, timeoutMs: number) {\n super(`GoDX platform request to ${url} did not answer within ${timeoutMs} ms`);\n this.name = \"GodxTimeoutError\";\n this.url = url;\n this.timeoutMs = timeoutMs;\n }\n}\n\n/**\n * `fetch` with {@link HttpOptions} applied: merged headers, a deadline, an injectable fetch.\n * Every outbound call in this package goes through here, so one option reaches all of them.\n */\nexport async function platformFetch(\n input: URL | string,\n init: RequestInit = {},\n options: HttpOptions = {},\n): Promise<Response> {\n const headers = new Headers(init.headers);\n const extra = typeof options.headers === \"function\" ? options.headers() : options.headers;\n for (const [name, value] of Object.entries(extra ?? {})) {\n if (!headers.has(name)) headers.set(name, value);\n }\n const doFetch = options.fetch ?? globalThis.fetch;\n if (options.timeoutMs === undefined || options.timeoutMs <= 0) {\n return doFetch(input, { ...init, headers });\n }\n const controller = new AbortController();\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, options.timeoutMs);\n init.signal?.addEventListener(\"abort\", () => controller.abort(), { once: true });\n try {\n return await doFetch(input, { ...init, headers, signal: controller.signal });\n } catch (error) {\n if (timedOut) throw new GodxTimeoutError(String(input), options.timeoutMs);\n throw error;\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport interface IssuerMetadata {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n userinfo_endpoint: string;\n jwks_uri: string;\n end_session_endpoint?: string;\n revocation_endpoint?: string;\n}\n\nexport interface Issuer {\n metadata: IssuerMetadata;\n jwks: ReturnType<typeof createRemoteJWKSet>;\n}\n\nexport class GodxHttpError extends Error {\n readonly status: number;\n readonly body: string;\n\n constructor(status: number, body: string, message?: string) {\n super(message ?? `GoDX platform request failed with ${status}`);\n this.name = \"GodxHttpError\";\n this.status = status;\n this.body = body;\n }\n}\n\n// Per-isolate cache; the platform docs require reading discovery, not hardcoding paths.\nconst issuerCache = new Map<string, Promise<Issuer>>();\n\n/**\n * Discovery + JWKS, cached per isolate and issuer. `options` applies to the discovery request and\n * to every JWKS refresh of the cached set (`timeoutMs` → jose `timeoutDuration`; static headers\n * only — a header FUNCTION cannot run inside jose's refresh, so it is evaluated once here). The\n * first call for an issuer decides the JWKS options; a failed load evicts itself.\n */\nexport function loadIssuer(issuerUrl: string, options: HttpOptions = {}): Promise<Issuer> {\n let cached = issuerCache.get(issuerUrl);\n if (!cached) {\n cached = (async () => {\n const res = await platformFetch(new URL(\"/.well-known/openid-configuration\", issuerUrl), {}, options);\n if (!res.ok) {\n issuerCache.delete(issuerUrl);\n throw new GodxHttpError(res.status, await res.text(), \"OIDC discovery failed\");\n }\n const metadata = (await res.json()) as IssuerMetadata;\n const jwksHeaders = typeof options.headers === \"function\" ? options.headers() : options.headers;\n const jwks = createRemoteJWKSet(new URL(metadata.jwks_uri), {\n ...(options.timeoutMs ? { timeoutDuration: options.timeoutMs } : {}),\n ...(jwksHeaders ? { headers: jwksHeaders } : {}),\n // jose keys its fetch override by its OWN exported symbol (a plain Symbol(), not\n // Symbol.for): 0.10.0 passed Symbol.for(\"jose.fetch\") and the JWKS fetch silently ignored\n // `http.fetch` — caught by pages' lead (godx-jp/server-sdk#19).\n ...(options.fetch ? { [customFetch]: options.fetch } : {}),\n });\n return { metadata, jwks };\n })();\n issuerCache.set(issuerUrl, cached);\n cached.catch(() => issuerCache.delete(issuerUrl));\n }\n return cached;\n}\n\nexport interface SsoClientConfig {\n issuer: string;\n serviceSlug: string;\n clientId: string;\n /** Omit for a public (secretless) instance — PKCE alone then. */\n clientSecret?: string;\n redirectUri: string;\n /** Token audience = the ServiceInstance slug (defaults to serviceSlug). */\n audience?: string;\n /** Headers, deadline and fetch for every request this client makes. */\n http?: HttpOptions;\n}\n\nexport interface TokenResponse {\n access_token: string;\n token_type: string;\n expires_in: number;\n id_token?: string;\n refresh_token?: string;\n scope?: string;\n}\n\nexport interface UserinfoResponse {\n sub: string;\n email?: string;\n email_verified?: boolean;\n name?: string;\n locale?: string;\n timezone?: string;\n}\n\nexport class GodxSsoClient {\n readonly config: SsoClientConfig;\n\n constructor(config: SsoClientConfig) {\n this.config = config;\n }\n\n async authorizeUrl(params: {\n state: string;\n nonce: string;\n codeChallenge: string;\n scope?: string;\n /** console_organization_id — required by the launch gates; usually arrives on the launch URL. */\n organizationContextId?: string;\n }): Promise<string> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n const url = new URL(metadata.authorization_endpoint);\n url.search = new URLSearchParams({\n service_slug: this.config.serviceSlug,\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n response_type: \"code\",\n scope: params.scope ?? \"openid profile email\",\n state: params.state,\n nonce: params.nonce,\n code_challenge: params.codeChallenge,\n code_challenge_method: \"S256\",\n ...(params.organizationContextId\n ? { organization_context_id: params.organizationContextId }\n : {}),\n }).toString();\n return url.toString();\n }\n\n async exchangeCode(params: { code: string; codeVerifier: string }): Promise<TokenResponse> {\n return this.tokenRequest(\"token_endpoint\", {\n grant_type: \"authorization_code\",\n redirect_uri: this.config.redirectUri,\n code: params.code,\n code_verifier: params.codeVerifier,\n });\n }\n\n async refresh(refreshToken: string): Promise<TokenResponse> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n return this.postForm(new URL(\"/api/sso/refresh\", metadata.issuer).toString(), {\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n // The platform files a refresh token under the INSTANCE slug (the token's audience,\n // e.g. \"godx-logger-prod\"), not the catalog service slug: sending the service slug\n // finds no row and answers invalid_grant (measured against id.godx.jp).\n service_slug: this.config.audience ?? this.config.serviceSlug,\n });\n }\n\n /** Best-effort revocation; failures are the caller's decision to ignore. */\n async revoke(token: string): Promise<void> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n if (!metadata.revocation_endpoint) return;\n await this.postForm(metadata.revocation_endpoint, { token });\n }\n\n async userinfo(accessToken: string): Promise<UserinfoResponse> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n const res = await platformFetch(metadata.userinfo_endpoint, {\n headers: { Authorization: `Bearer ${accessToken}` },\n }, this.config.http);\n if (!res.ok) throw new GodxHttpError(res.status, await res.text(), \"userinfo failed\");\n return (await res.json()) as UserinfoResponse;\n }\n\n private async tokenRequest(\n endpoint: \"token_endpoint\",\n fields: Record<string, string>,\n ): Promise<TokenResponse> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n return this.postForm(metadata[endpoint], fields);\n }\n\n private async postForm(url: string, fields: Record<string, string>): Promise<TokenResponse> {\n const body = new URLSearchParams({\n service_slug: this.config.serviceSlug,\n client_id: this.config.clientId,\n ...(this.config.clientSecret ? { client_secret: this.config.clientSecret } : {}),\n ...fields,\n });\n const res = await platformFetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n }, this.config.http);\n if (!res.ok) throw new GodxHttpError(res.status, await res.text(), \"token request failed\");\n return (await res.json()) as TokenResponse;\n }\n}\n\n/**\n * Server-to-server reads with service credentials (X-Service-Slug/X-Service-Secret).\n * Rate-limited by the platform (429) — cache results and back off; no auto-retry here.\n */\nexport function createServiceApi(config: {\n issuer: string;\n serviceSlug: string;\n serviceSecret: string;\n http?: HttpOptions;\n}) {\n const get = async <T>(path: string, query: Record<string, string>): Promise<T> => {\n const url = new URL(path, config.issuer);\n url.search = new URLSearchParams(query).toString();\n const res = await platformFetch(url, {\n headers: {\n \"X-Service-Slug\": config.serviceSlug,\n \"X-Service-Secret\": config.serviceSecret,\n },\n }, config.http);\n if (!res.ok) throw new GodxHttpError(res.status, await res.text(), `service API ${path} failed`);\n return (await res.json()) as T;\n };\n\n return {\n users: (q: { organizationSlug: string; perPage?: number; page?: number }) =>\n get<unknown>(\"/api/sso/service/users\", {\n organization_slug: q.organizationSlug,\n ...(q.perPage ? { per_page: String(q.perPage) } : {}),\n ...(q.page ? { page: String(q.page) } : {}),\n }),\n branches: (q: { organizationSlug: string }) =>\n get<unknown>(\"/api/sso/service/branches\", { organization_slug: q.organizationSlug }),\n brands: (q: { organizationSlug: string }) =>\n get<unknown>(\"/api/sso/service/brands\", { organization_slug: q.organizationSlug }),\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/auth.ts"],"sourcesContent":["/**\n * ./auth — verified immutable Principal, PKCE, and sealed session values.\n * Mirrors php-sdk's Auth module: the Principal only comes out of one parse\n * gate (verifyAccessToken) and is frozen after that.\n */\nimport { EncryptJWT, jwtDecrypt, jwtVerify } from \"jose\";\nimport { type HttpOptions, loadIssuer } from \"./http\";\n\nexport interface Principal {\n readonly sub: string;\n /** Claim organization_id — the organization's INTERNAL id. */\n readonly organizationId: string;\n /**\n * Claim organization_context_id — the CONSOLE organization id, when the token carries one.\n * This is the id the platform's organization-scoped APIs look organizations up by\n * (`/api/sso/me/permissions`, `fetchEffectivePermissions`); passing `organizationId` there\n * answers 403 CONTEXT_UNAVAILABLE (godx-jp/server-sdk#6).\n */\n readonly organizationContextId?: string;\n /** Defaults to read_only unless the claim says full — fail closed. */\n readonly accessMode: \"full\" | \"read_only\";\n readonly scopes: readonly string[];\n readonly serviceInstanceId?: string;\n readonly clientId?: string;\n /** Unix seconds, from the access token's exp. */\n readonly expiresAt: number;\n /** From userinfo, when the adapter fetched it at login. */\n readonly name?: string;\n readonly email?: string;\n /** The exact `iss` the token was verified against (discovery issuer). Absent on sessions sealed before 0.11.0. */\n readonly issuer?: string;\n /** The audience the token was verified against — your resource URI / instance slug. Absent on sessions sealed before 0.11.0. */\n readonly resource?: string;\n /** Claim `jti`. */\n readonly tokenId?: string;\n /** Unix seconds, from `iat`. */\n readonly issuedAt?: number;\n /**\n * Claim `gx_execution_id` — set only on a gx MUTATION token (a one-call execution token or a\n * checkpointed one); never on a read token. The service's once-only gate keys on it (godx-jp/id#2190).\n */\n readonly executionId?: string;\n /** Claim `gx_arguments_sha256` — SHA-256 hex of the approved arguments in canonical form (one-call execution token). */\n readonly argumentsDigest?: string;\n /**\n * Claim `service_authorization` of a one-call execution token: byte-for-byte what\n * `POST /api/sso/introspect` would answer for this token, computed live at minting (F = 0). Only\n * present when its `execution_id` names THIS token's execution and its `service` is THIS audience —\n * otherwise it is dropped at parse and the caller falls back to `fetchEffectivePermissions`.\n */\n readonly serviceAuthorization?: EmbeddedAuthorization;\n}\n\n/** The `service_authorization` snapshot Platform embeds in a one-call execution token (godx-jp/id#2190). */\nexport interface EmbeddedAuthorization {\n readonly authoritative: true;\n readonly service: string;\n readonly role: string | null;\n readonly permissions: readonly string[];\n readonly branchId: string | null;\n /** ISO-8601, the moment Platform computed the snapshot — a few ms before `iat`. */\n readonly evaluatedAt: string;\n readonly executionId: string;\n}\n\n/**\n * The one parse gate. Validates signature (JWKS), iss (discovery issuer,\n * exact), aud (= service slug), exp — then requires organization_id.\n */\nexport async function verifyAccessToken(\n token: string,\n opts: {\n issuer: string;\n serviceSlug: string;\n /** The token audience = your ServiceInstance slug. Defaults to serviceSlug. */\n audience?: string;\n /** Headers, deadline and fetch for discovery and JWKS (first call per issuer decides the JWKS options). */\n http?: HttpOptions;\n },\n): Promise<Principal> {\n const expectedAudience = opts.audience ?? opts.serviceSlug;\n const { metadata, jwks } = await loadIssuer(opts.issuer, opts.http);\n let payload;\n try {\n ({ payload } = await jwtVerify(token, jwks, {\n issuer: metadata.issuer,\n audience: expectedAudience,\n }));\n } catch (e) {\n // Say what was expected vs what the token actually carries — a bare\n // \"unexpected claim\" costs whoever reads it a debugging session.\n const actual = decodeUnverified(token);\n throw new Error(\n `${e instanceof Error ? e.message : String(e)} — expected iss=\"${metadata.issuer}\" aud=\"${expectedAudience}\", ` +\n `token carries iss=\"${String(actual?.iss ?? \"?\")}\" aud=${JSON.stringify(actual?.aud ?? \"?\")}`,\n { cause: e },\n );\n }\n\n const organizationId = payload[\"organization_id\"];\n if (typeof organizationId !== \"string\" || organizationId === \"\") {\n throw new Error(\"access token has no organization_id claim\");\n }\n if (typeof payload.sub !== \"string\" || typeof payload.exp !== \"number\") {\n throw new Error(\"access token is missing sub/exp\");\n }\n\n const executionId = nonEmptyString(payload[\"gx_execution_id\"]);\n return Object.freeze({\n sub: payload.sub,\n issuer: metadata.issuer,\n resource: expectedAudience,\n tokenId: nonEmptyString(payload.jti),\n issuedAt: typeof payload.iat === \"number\" ? payload.iat : undefined,\n executionId,\n argumentsDigest: nonEmptyString(payload[\"gx_arguments_sha256\"]),\n serviceAuthorization: embeddedAuthorization(payload[\"service_authorization\"], executionId, expectedAudience),\n organizationId,\n organizationContextId:\n typeof payload[\"organization_context_id\"] === \"string\" && payload[\"organization_context_id\"] !== \"\"\n ? payload[\"organization_context_id\"]\n : undefined,\n accessMode: payload[\"organization_access_mode\"] === \"full\" ? \"full\" : \"read_only\",\n scopes: Object.freeze(String(payload[\"scope\"] ?? \"\").split(\" \").filter(Boolean)),\n serviceInstanceId: typeof payload[\"service_instance_id\"] === \"string\" ? payload[\"service_instance_id\"] : undefined,\n clientId: typeof payload[\"client_id\"] === \"string\" ? payload[\"client_id\"] : undefined,\n expiresAt: payload.exp,\n } satisfies Principal);\n}\n\nexport interface Pkce {\n verifier: string;\n challenge: string;\n method: \"S256\";\n}\n\nexport async function createPkce(): Promise<Pkce> {\n const verifier = randomToken(32);\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(verifier));\n return { verifier, challenge: base64url(new Uint8Array(digest)), method: \"S256\" };\n}\n\nexport function randomToken(bytes = 16): string {\n const buf = new Uint8Array(bytes);\n crypto.getRandomValues(buf);\n return base64url(buf);\n}\n\nfunction nonEmptyString(value: unknown): string | undefined {\n return typeof value === \"string\" && value !== \"\" ? value : undefined;\n}\n\n/**\n * Accept the embedded snapshot only when it is provably THIS token's and THIS service's — the same\n * four checks php-sdk's TokenEmbeddedPermissionSource makes. Anything else → undefined (fall back to\n * introspection); never a partial object.\n */\nfunction embeddedAuthorization(\n raw: unknown,\n executionId: string | undefined,\n resource: string,\n): EmbeddedAuthorization | undefined {\n if (!executionId || typeof raw !== \"object\" || raw === null) return undefined;\n const snapshot = raw as Record<string, unknown>;\n const permissions = snapshot[\"permissions\"];\n if (\n snapshot[\"authoritative\"] !== true ||\n snapshot[\"service\"] !== resource ||\n snapshot[\"execution_id\"] !== executionId ||\n !Array.isArray(permissions) ||\n !permissions.every((p): p is string => typeof p === \"string\") ||\n typeof snapshot[\"evaluated_at\"] !== \"string\"\n ) {\n return undefined;\n }\n return Object.freeze({\n authoritative: true,\n service: resource,\n role: typeof snapshot[\"role\"] === \"string\" ? snapshot[\"role\"] : null,\n permissions: Object.freeze([...permissions]),\n branchId: typeof snapshot[\"branch_id\"] === \"string\" ? snapshot[\"branch_id\"] : null,\n evaluatedAt: snapshot[\"evaluated_at\"],\n executionId,\n });\n}\n\n/** Diagnostics only — an UNVERIFIED read of the payload for error messages. */\nfunction decodeUnverified(token: string): Record<string, unknown> | null {\n try {\n const part = token.split(\".\")[1] ?? \"\";\n const pad = part.replace(/-/g, \"+\").replace(/_/g, \"/\");\n return JSON.parse(atob(pad)) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n return btoa(bin).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function sessionKey(secret: string): Promise<Uint8Array> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(secret));\n return new Uint8Array(digest);\n}\n\n/** Encrypt + sign an arbitrary payload into a compact cookie value (JWE dir/A256GCM). */\nexport async function sealData(\n payload: Record<string, unknown>,\n secret: string,\n ttlSeconds: number,\n): Promise<string> {\n return new EncryptJWT(payload)\n .setProtectedHeader({ alg: \"dir\", enc: \"A256GCM\" })\n .setIssuedAt()\n .setExpirationTime(Math.floor(Date.now() / 1000) + ttlSeconds)\n .encrypt(await sessionKey(secret));\n}\n\n/** Returns null for anything invalid or expired — never throws on bad input. */\nexport async function openData<T = Record<string, unknown>>(\n token: string,\n secret: string,\n): Promise<T | null> {\n try {\n const { payload } = await jwtDecrypt(token, await sessionKey(secret));\n return payload as T;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":[]}
|