@godxjp/server-sdk 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/dist/notifications.d.ts +269 -0
- package/dist/notifications.js +418 -0
- package/dist/notifications.js.map +1 -0
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ Naming is by **side**, not by language (`web-sdk` / `server-sdk` / `php-sdk`)
|
|
|
20
20
|
| `./hono` | Thin adapter: `godxSso()` mounts `/auth/{login,callback,logout}`, `requireAuth`, `requireWritable` (read-only organizations may not write), `dockProxy` (the GoDX Dock's `apiBase` lane — no CORS, no token in the page). Every SSO failure lands back on the app as `/?sso_error=…&sso_error_description=…` with a human-readable description. |
|
|
21
21
|
| `./sync` | The identity feed, both consumer legs — see below. |
|
|
22
22
|
| `./org` | Typed client for the **write-back** surface (`/api/org/v1/*`): me, employees, departments, branches, teams (list / paginate-all / create), and **notifications.create** (godx-jp/id#1962). `Idempotency-Key` on every write (IETF draft), RFC 6750 `WWW-Authenticate` surfaced on 401/403, 422 field errors and 429 `Retry-After` exposed on `OrgApiError`. Tenancy is the token's — the client never sends an organization id. Since godx-jp/id#1960, a downstream **service-launch token carries the `organization-api` audience whenever org.\* scopes are granted** (i.e. survive RBAC narrowing) — see [`./org`](#org--the-write-back-client) below. |
|
|
23
|
+
| `./notifications` | **Notification Center**, one interface for every service (CHAT-65), mirroring php-sdk: an immutable `Notice` checked against Platform's rules (`InvalidNoticeError` names the field), `createNotificationClient` (machine lane, `NotificationError.reason` = php-sdk's `NotificationFailure`), and `handleNoticeAction` for quick-action presses. Shared fixtures `contract/notices` — see [`./notifications`](#notifications--notification-center) below. |
|
|
23
24
|
| `./producer` | The **outbox producer kit** (godx-jp/id#1963 phase 4): `OUTBOX_TABLE_SQL` (D1 DDL), `recordDomainEvent` (write the event next to the state change), `drainOutbox` (relay unpublished rows in sequence order through an injected `publish`). CloudEvents 1.0 envelope mirroring `./sync`'s shape — see [`./producer`](#producer--the-outbox-producer-kit) below. |
|
|
24
25
|
|
|
25
26
|
Core modules depend on `fetch` + WebCrypto only; `hono` is the only framework-shaped subpath.
|
|
@@ -253,6 +254,73 @@ and the scope→permission mapping.
|
|
|
253
254
|
| `recordDomainEvent` | `(db, {service, organizationId, aggregate, action, data, id?}) => Promise<RecordedDomainEvent>` | Insert one outbox row next to the caller's own write; composes subject/type, validates every token. |
|
|
254
255
|
| `drainOutbox` | `(db, publish, {batch = 100}?) => Promise<{published, remaining}>` | Publish unpublished rows in sequence order; marks `published_at` only after `publish` resolves; stops at the first failure. |
|
|
255
256
|
|
|
257
|
+
## `./notifications` — Notification Center
|
|
258
|
+
|
|
259
|
+
Put a notice in a person's Dock bell and receive their quick-action presses — the same contract as
|
|
260
|
+
php-sdk's `Notifications\Notice`, `Http\NotificationClient` and `Route::godxNoticeActions()`
|
|
261
|
+
(Platform: godx-jp/id `docs/guide/downstream-service-onboarding.md` → Sending notifications).
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { Notice, NotificationError, createNotificationClient, handleNoticeAction, NOTICE_ACTIONS_PATH } from "@godxjp/server-sdk/notifications";
|
|
265
|
+
|
|
266
|
+
const notifications = createNotificationClient({
|
|
267
|
+
issuer: env.GODX_ISSUER,
|
|
268
|
+
serviceSlug: env.GODX_INSTANCE_SLUG, // the INSTANCE slug, never client_id
|
|
269
|
+
serviceSecret: env.GODX_CLIENT_SECRET, // empty → `configured` is false and send() rejects `not_configured`
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const notice = Notice.create({
|
|
273
|
+
recipients: [approverId], // Platform user ids, 1..100
|
|
274
|
+
sourceRef: `approval:task:${taskId}`, // the dedup key: one row per ref per person
|
|
275
|
+
actionUrl: `${baseUrl}/requests/42`, // absolute, under the instance base URL
|
|
276
|
+
title: { ja: "承認依頼", en: "Approval requested" },
|
|
277
|
+
body: { ja: "休暇申請 #42 が届きました。" },
|
|
278
|
+
})
|
|
279
|
+
.withKind("approval")
|
|
280
|
+
.forOrganization(consoleOrganizationId) // pool deployments
|
|
281
|
+
.withActions(
|
|
282
|
+
{ id: "approve", label: { ja: "承認", en: "Approve" }, style: "primary" },
|
|
283
|
+
{ id: "reject", label: { ja: "却下", en: "Reject" }, style: "danger", reason: "required" },
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const { created, updated, skipped } = await notifications.send(notice, { idempotencyKey: `approval:task:${taskId}:1` });
|
|
288
|
+
} catch (e) {
|
|
289
|
+
if (e instanceof NotificationError && e.retryable) { /* rate_limited / unavailable: retry with the SAME key */ }
|
|
290
|
+
throw e;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// The press: POST {base_url}/api/platform/v1/notice-actions
|
|
294
|
+
app.post(NOTICE_ACTIONS_PATH, (c) =>
|
|
295
|
+
handleNoticeAction(c.req.raw, {
|
|
296
|
+
issuer: c.env.GODX_ISSUER,
|
|
297
|
+
audience: c.env.GODX_INSTANCE_SLUG,
|
|
298
|
+
handler: async (press) => {
|
|
299
|
+
// press.principal.subject / .organization come from the token; press.sourceRef, .action, .reason, .locale
|
|
300
|
+
if (press.action !== "approve" && press.action !== "reject") return { state: "invalid" };
|
|
301
|
+
return decide(press); // as that person, under your own rules → done | already_done | denied | invalid
|
|
302
|
+
},
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
- **Send.** `Notice` is immutable (`with…()` returns a new, re-checked one) and `toPayload()` emits only the fields that
|
|
308
|
+
were set — Platform fingerprints the body for its `Idempotency-Key`. `Notice.fromPayload(body)` reads a hand-built body,
|
|
309
|
+
refusing an unknown key or locale that Platform would silently drop. Lengths are counted in code points, as Laravel does.
|
|
310
|
+
- **Failures.** `NotificationError extends GodxHttpError`; `reason` is one of `not_configured`, `unauthenticated`,
|
|
311
|
+
`forbidden` (the lane is off — `notifications_api_enabled` — or the credential is wrong; Platform answers both alike),
|
|
312
|
+
`organization_not_served`, `idempotency_conflict`, `invalid`, `rate_limited`, `unavailable`, `unexpected_response`.
|
|
313
|
+
Like php-sdk, the error body is never read (`body` is `""`): a 422 for one recipient without access refuses the whole
|
|
314
|
+
call and does not say which. Redirects are not followed.
|
|
315
|
+
- **Receive.** `handleNoticeAction` verifies the token as php-sdk's `AuthenticateResource` does (RS256, `typ` at+jwt,
|
|
316
|
+
issuer from discovery, `aud`, `exp`, 30 s leeway) — not with `verifyAccessToken`, because Platform mints this token
|
|
317
|
+
without `organization_id`. It admits only a person (`prn` user) from `platform-notice-actions` with
|
|
318
|
+
`notices.actions:execute` (else `403 {"error":"forbidden"}`), reads the body (malformed: `422 {"state":"invalid"}`), and
|
|
319
|
+
answers `{state, message?}`: 200, or 422 for `invalid`. `done` / `already_done` settle the notice on Platform for good —
|
|
320
|
+
answer `already_done` only when the effect is confirmed, and never treat an unknown action as one you know.
|
|
321
|
+
- **One contract.** `contract/notices/` is byte-identical to php-sdk's `tests/Fixtures/notices/`; both suites run every
|
|
322
|
+
case and pin the same digest of the set. Change both in the same release.
|
|
323
|
+
|
|
256
324
|
## `./sync` — the identity feed
|
|
257
325
|
|
|
258
326
|
The platform publishes identity/directory changes (organizations, branches, brands, users,
|
|
@@ -635,6 +703,8 @@ npx vitest run # from packages/server-sdk
|
|
|
635
703
|
- `org.test.ts` (9): Bearer + Accept headers, Idempotency-Key (generated and caller-supplied),
|
|
636
704
|
pagination to the last page, 403 `WWW-Authenticate` surfaced, 401 audience hint, 422 per-field
|
|
637
705
|
validation, 429 `Retry-After`, notifications.create mapping + Idempotency-Key.
|
|
706
|
+
- `notifications.test.ts`: every shared notice fixture (valid and invalid bodies, Platform's answers, signed presses),
|
|
707
|
+
the typed builder, redirect/network/not-configured handling.
|
|
638
708
|
- `producer.test.ts` (8): subject/type composition, token charset refusal naming the offender,
|
|
639
709
|
sequence monotonicity, drain order + envelope shape (no tenantid), stop-on-first-failure,
|
|
640
710
|
published_at only after publish resolves, batch limit, DDL ships the drain index.
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { HttpOptions, GodxHttpError } from './http.js';
|
|
2
|
+
import 'jose';
|
|
3
|
+
|
|
4
|
+
type NoticeLocale = "ja" | "en" | "vi";
|
|
5
|
+
/** Copy in the locales Platform renders: `ja` required, `en`/`vi` optional. Plain text only. */
|
|
6
|
+
interface LocalizedText {
|
|
7
|
+
readonly ja: string;
|
|
8
|
+
readonly en?: string | null;
|
|
9
|
+
readonly vi?: string | null;
|
|
10
|
+
}
|
|
11
|
+
/** Platform's closed categories; `service` is the one for a notice a service authors. */
|
|
12
|
+
type NoticeCategory = "service" | "organization_invitation" | "access_request" | "service_provisioning" | "billing" | "security" | "operator";
|
|
13
|
+
type NoticeActionStyle = "default" | "primary" | "danger";
|
|
14
|
+
/** Whether a press carries a reason; Platform enforces it before calling the service. */
|
|
15
|
+
type NoticeActionReason = "none" | "optional" | "required";
|
|
16
|
+
/** One quick-action button (at most three on a notice). Only a service instance may offer them. */
|
|
17
|
+
interface NoticeActionButton {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly label: LocalizedText;
|
|
20
|
+
readonly style?: NoticeActionStyle;
|
|
21
|
+
readonly reason?: NoticeActionReason;
|
|
22
|
+
}
|
|
23
|
+
/** Platform's wire body, in the shape both SDKs emit: only fields that were set. */
|
|
24
|
+
interface NoticePayload {
|
|
25
|
+
recipients: string[];
|
|
26
|
+
category: NoticeCategory;
|
|
27
|
+
source_ref: string;
|
|
28
|
+
action_url: string;
|
|
29
|
+
title: Partial<Record<NoticeLocale, string>> & {
|
|
30
|
+
ja: string;
|
|
31
|
+
};
|
|
32
|
+
body: Partial<Record<NoticeLocale, string>> & {
|
|
33
|
+
ja: string;
|
|
34
|
+
};
|
|
35
|
+
kind?: string;
|
|
36
|
+
context?: Partial<Record<NoticeLocale, string>> & {
|
|
37
|
+
ja: string;
|
|
38
|
+
};
|
|
39
|
+
actions?: {
|
|
40
|
+
id: string;
|
|
41
|
+
label: Partial<Record<NoticeLocale, string>> & {
|
|
42
|
+
ja: string;
|
|
43
|
+
};
|
|
44
|
+
style: NoticeActionStyle;
|
|
45
|
+
reason: NoticeActionReason;
|
|
46
|
+
}[];
|
|
47
|
+
organization_id?: string;
|
|
48
|
+
}
|
|
49
|
+
/** A notice Platform would refuse (or silently drop part of); `field` is Laravel's dot-notation key. */
|
|
50
|
+
declare class InvalidNoticeError extends Error {
|
|
51
|
+
readonly field: string;
|
|
52
|
+
constructor(field: string, reason: string);
|
|
53
|
+
}
|
|
54
|
+
interface NoticeFields {
|
|
55
|
+
readonly recipients: readonly string[];
|
|
56
|
+
readonly sourceRef: string;
|
|
57
|
+
readonly actionUrl: string;
|
|
58
|
+
readonly title: LocalizedText;
|
|
59
|
+
readonly body: LocalizedText;
|
|
60
|
+
readonly category: NoticeCategory;
|
|
61
|
+
readonly kind: string | null;
|
|
62
|
+
readonly context: LocalizedText | null;
|
|
63
|
+
readonly actions: readonly NoticeActionButton[];
|
|
64
|
+
readonly organizationId: string | null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* One notice, immutable: each `with…()` returns a new one, checked again.
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* const notice = Notice.create({
|
|
71
|
+
* recipients: [approverId], // Platform user ids, 1..100
|
|
72
|
+
* sourceRef: `approval:task:${taskId}`, // the dedup key: one row per ref per person
|
|
73
|
+
* actionUrl: `${baseUrl}/requests/42`, // absolute, under the instance base URL
|
|
74
|
+
* title: { ja: "承認依頼", en: "Approval requested" },
|
|
75
|
+
* body: { ja: "休暇申請 #42 が届きました。" },
|
|
76
|
+
* })
|
|
77
|
+
* .withKind("approval")
|
|
78
|
+
* .withActions(
|
|
79
|
+
* { id: "approve", label: { ja: "承認", en: "Approve" }, style: "primary" },
|
|
80
|
+
* { id: "reject", label: { ja: "却下", en: "Reject" }, style: "danger", reason: "required" },
|
|
81
|
+
* );
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* Every recipient must be a member of the organization with access to the service, or Platform
|
|
85
|
+
* refuses the WHOLE call (422) — checked there, not here. Sending the same `sourceRef` again
|
|
86
|
+
* rewrites that person's row and marks it unread, without a second email.
|
|
87
|
+
*/
|
|
88
|
+
declare class Notice implements NoticeFields {
|
|
89
|
+
readonly recipients: readonly string[];
|
|
90
|
+
readonly sourceRef: string;
|
|
91
|
+
readonly actionUrl: string;
|
|
92
|
+
readonly title: LocalizedText;
|
|
93
|
+
readonly body: LocalizedText;
|
|
94
|
+
readonly category: NoticeCategory;
|
|
95
|
+
readonly kind: string | null;
|
|
96
|
+
readonly context: LocalizedText | null;
|
|
97
|
+
readonly actions: readonly NoticeActionButton[];
|
|
98
|
+
readonly organizationId: string | null;
|
|
99
|
+
private constructor();
|
|
100
|
+
/** @throws InvalidNoticeError */
|
|
101
|
+
static create(input: {
|
|
102
|
+
recipients: readonly string[];
|
|
103
|
+
sourceRef: string;
|
|
104
|
+
actionUrl: string;
|
|
105
|
+
title: LocalizedText;
|
|
106
|
+
body: LocalizedText;
|
|
107
|
+
}): Notice;
|
|
108
|
+
/**
|
|
109
|
+
* Read a body in Platform's wire shape. Stricter than Platform on purpose: an unknown key or
|
|
110
|
+
* locale, which Platform would silently drop, is refused.
|
|
111
|
+
*
|
|
112
|
+
* @throws InvalidNoticeError
|
|
113
|
+
*/
|
|
114
|
+
static fromPayload(payload: unknown): Notice;
|
|
115
|
+
withCategory(category: NoticeCategory): Notice;
|
|
116
|
+
/** One of the kinds the service declared in its manifest (`notifications`), so a person can turn it off. */
|
|
117
|
+
withKind(kind: string | null): Notice;
|
|
118
|
+
/** A short line placing the notice, printed under the email body (e.g. `WEB-123 · Website`). */
|
|
119
|
+
withContext(context: LocalizedText | null): Notice;
|
|
120
|
+
/** Replaces the quick actions (at most three). A notice with actions is never mirrored to Renraku. */
|
|
121
|
+
withActions(...actions: NoticeActionButton[]): Notice;
|
|
122
|
+
/** The CONSOLE organization id the notice is about — required for a deployment serving many organizations. */
|
|
123
|
+
forOrganization(consoleOrganizationId: string | null): Notice;
|
|
124
|
+
/**
|
|
125
|
+
* Platform's body, with only the fields that were set: an absent optional field and an empty one
|
|
126
|
+
* are not the same body to Platform's Idempotency-Key fingerprint.
|
|
127
|
+
*/
|
|
128
|
+
toPayload(): NoticePayload;
|
|
129
|
+
private assertValid;
|
|
130
|
+
}
|
|
131
|
+
/** Why a notice was not delivered — the same strings as php-sdk's `NotificationFailure`. */
|
|
132
|
+
type NotificationFailureReason =
|
|
133
|
+
/** No instance credential was configured, so nothing was sent. */
|
|
134
|
+
"not_configured"
|
|
135
|
+
/** 401. */
|
|
136
|
+
| "unauthenticated"
|
|
137
|
+
/** 403: `notifications_api_enabled` is off (default) or the credential is wrong — Platform answers both alike. */
|
|
138
|
+
| "forbidden"
|
|
139
|
+
/** 403 `ORGANIZATION_NOT_SERVED`: forOrganization() names one this instance does not serve. */
|
|
140
|
+
| "organization_not_served"
|
|
141
|
+
/** 409: this Idempotency-Key was already used with another body. */
|
|
142
|
+
| "idempotency_conflict"
|
|
143
|
+
/** 422: Platform refused the body — usually one recipient without access, which refuses the whole call. */
|
|
144
|
+
| "invalid" | "rate_limited"
|
|
145
|
+
/** 5xx, a timeout or a network failure. The notice may or may not have been written. */
|
|
146
|
+
| "unavailable"
|
|
147
|
+
/** A redirect, an unexpected status, or a success whose body is not Platform's shape. */
|
|
148
|
+
| "unexpected_response";
|
|
149
|
+
/**
|
|
150
|
+
* A notice was not delivered. A {@link GodxHttpError}, so an existing catch keeps working; its
|
|
151
|
+
* `body` is always empty — like php-sdk, this client never reads an error body (Platform's have
|
|
152
|
+
* carried internal detail), only the `X-Godx-Error` code and `X-Request-Id`.
|
|
153
|
+
*/
|
|
154
|
+
declare class NotificationError extends GodxHttpError {
|
|
155
|
+
readonly reason: NotificationFailureReason;
|
|
156
|
+
readonly requestId: string | null;
|
|
157
|
+
constructor(reason: NotificationFailureReason, status?: number, requestId?: string | null, options?: {
|
|
158
|
+
cause?: unknown;
|
|
159
|
+
});
|
|
160
|
+
/**
|
|
161
|
+
* Whether sending again may succeed. Retry only with the SAME idempotency key and notice: an
|
|
162
|
+
* unavailable answer can follow a write that happened.
|
|
163
|
+
*/
|
|
164
|
+
get retryable(): boolean;
|
|
165
|
+
}
|
|
166
|
+
/** Platform's answer: rows inserted, rows rewritten (same `source_ref`), people skipped (`preference_off`). */
|
|
167
|
+
interface NoticeDelivery {
|
|
168
|
+
readonly created: readonly {
|
|
169
|
+
id: string;
|
|
170
|
+
user_id: string;
|
|
171
|
+
}[];
|
|
172
|
+
readonly updated: readonly {
|
|
173
|
+
id: string;
|
|
174
|
+
user_id: string;
|
|
175
|
+
}[];
|
|
176
|
+
readonly skipped: readonly {
|
|
177
|
+
user_id: string;
|
|
178
|
+
reason: string;
|
|
179
|
+
}[];
|
|
180
|
+
}
|
|
181
|
+
interface NotificationClientConfig {
|
|
182
|
+
issuer: string;
|
|
183
|
+
/** The INSTANCE slug (the token audience), never the client_id. */
|
|
184
|
+
serviceSlug: string;
|
|
185
|
+
/** The instance client_secret. */
|
|
186
|
+
serviceSecret: string;
|
|
187
|
+
/** Headers, deadline and fetch for every call. */
|
|
188
|
+
http?: HttpOptions;
|
|
189
|
+
}
|
|
190
|
+
interface NotificationClient {
|
|
191
|
+
/** Whether a credential is configured; without one, send() rejects with `not_configured`. */
|
|
192
|
+
readonly configured: boolean;
|
|
193
|
+
/**
|
|
194
|
+
* `idempotencyKey` (at most 191 characters): the same key and notice replay Platform's stored
|
|
195
|
+
* answer; the same key with another notice is `idempotency_conflict`.
|
|
196
|
+
*/
|
|
197
|
+
send(notice: Notice, options?: {
|
|
198
|
+
idempotencyKey?: string;
|
|
199
|
+
}): Promise<NoticeDelivery>;
|
|
200
|
+
}
|
|
201
|
+
declare const NOTIFICATIONS_PATH = "/api/sso/service/notifications";
|
|
202
|
+
/**
|
|
203
|
+
* The machine lane — the instance credential, gated by its capability `notifications_api_enabled`
|
|
204
|
+
* (default off). Redirects are never followed: a credential header must not reach another origin.
|
|
205
|
+
*/
|
|
206
|
+
declare function createNotificationClient(config: NotificationClientConfig): NotificationClient;
|
|
207
|
+
/**
|
|
208
|
+
* The service's answer to a press. `done` and `already_done` SETTLE the notice: Platform remembers
|
|
209
|
+
* it and replays it on the next press without calling again — answer `already_done` only when the
|
|
210
|
+
* effect is confirmed.
|
|
211
|
+
*/
|
|
212
|
+
type NoticeActionState = "done" | "already_done" | "denied" | "invalid";
|
|
213
|
+
interface NoticeActionResult {
|
|
214
|
+
readonly state: NoticeActionState;
|
|
215
|
+
/** One short line for the person, in the press's locale (Platform shows at most 500 characters). */
|
|
216
|
+
readonly message?: string | null;
|
|
217
|
+
}
|
|
218
|
+
/** One press, after the token was verified as Platform's notice-action token for a person. */
|
|
219
|
+
interface NoticeAction {
|
|
220
|
+
/** Who pressed (`sub`) and in which organization (`organization_context_id`, the console id) — from the token, never the body. */
|
|
221
|
+
readonly principal: {
|
|
222
|
+
readonly subject: string;
|
|
223
|
+
readonly organization: string;
|
|
224
|
+
readonly clientId: string;
|
|
225
|
+
readonly scopes: readonly string[];
|
|
226
|
+
};
|
|
227
|
+
readonly sourceRef: string;
|
|
228
|
+
/** The button id. */
|
|
229
|
+
readonly action: string;
|
|
230
|
+
/** Null when none was written. */
|
|
231
|
+
readonly reason: string | null;
|
|
232
|
+
/** The language to answer `message` in. */
|
|
233
|
+
readonly locale: NoticeLocale | null;
|
|
234
|
+
readonly noticeId: string | null;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Platform decided NOTHING: it checked only that the person may still open the service and that the
|
|
238
|
+
* notice offered this button. The handler, as `press.principal` and under the service's own rules,
|
|
239
|
+
* finds the subject by `sourceRef` WITHIN `press.principal.organization`, answers `invalid` for an
|
|
240
|
+
* action it does not offer, re-checks the person's right (`denied`), and makes the change
|
|
241
|
+
* atomically. A thrown error is a 500, which the person sees as a failure; nothing is settled.
|
|
242
|
+
*/
|
|
243
|
+
type NoticeActionHandler = (press: NoticeAction) => NoticeActionResult | Promise<NoticeActionResult>;
|
|
244
|
+
declare const NOTICE_ACTIONS_PATH = "/api/platform/v1/notice-actions";
|
|
245
|
+
interface NoticeActionOptions {
|
|
246
|
+
issuer: string;
|
|
247
|
+
/** Your instance slug — the token's `aud`. */
|
|
248
|
+
audience: string;
|
|
249
|
+
handler: NoticeActionHandler;
|
|
250
|
+
/** Default `platform-notice-actions`. */
|
|
251
|
+
clientId?: string;
|
|
252
|
+
/** Default `notices.actions:execute`. */
|
|
253
|
+
scope?: string;
|
|
254
|
+
/** Headers, deadline and fetch for discovery and JWKS. */
|
|
255
|
+
http?: HttpOptions;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Answers Platform's `POST {base_url}/api/platform/v1/notice-actions` (Fetch API: Workers, Node 18+;
|
|
259
|
+
* Hono: `app.post(NOTICE_ACTIONS_PATH, (c) => handleNoticeAction(c.req.raw, options))`).
|
|
260
|
+
*
|
|
261
|
+
* The token is verified like php-sdk's `AuthenticateResource` does (RS256, `typ` at+jwt, exact
|
|
262
|
+
* issuer from discovery, `aud`, `exp`; else 401) — not with {@link verifyAccessToken}, because
|
|
263
|
+
* Platform mints this token without `organization_id`. Then it must be a PERSON's (`prn` user)
|
|
264
|
+
* from `clientId` carrying `scope`, else `403 {"error":"forbidden"}`; a malformed body is
|
|
265
|
+
* `422 {"state":"invalid"}`. The handler's answer is `{state, message?}`: 200, or 422 for `invalid`.
|
|
266
|
+
*/
|
|
267
|
+
declare function handleNoticeAction(request: Request, options: NoticeActionOptions): Promise<Response>;
|
|
268
|
+
|
|
269
|
+
export { InvalidNoticeError, type LocalizedText, NOTICE_ACTIONS_PATH, NOTIFICATIONS_PATH, Notice, type NoticeAction, type NoticeActionButton, type NoticeActionHandler, type NoticeActionOptions, type NoticeActionReason, type NoticeActionResult, type NoticeActionState, type NoticeActionStyle, type NoticeCategory, type NoticeDelivery, type NoticeLocale, type NoticePayload, type NotificationClient, type NotificationClientConfig, NotificationError, type NotificationFailureReason, createNotificationClient, handleNoticeAction };
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/server-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
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",
|