@stringpush/sdk 0.1.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 ADDED
@@ -0,0 +1,134 @@
1
+ # @stringpush/sdk
2
+
3
+ Framework-agnostic plain JavaScript SDK for the Translation Platform.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @stringpush/sdk
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { init, setLocale, t } from "@stringpush/sdk";
15
+
16
+ await init({
17
+ applicationId: "your-application-id",
18
+ environment: "staging",
19
+ locale: "en",
20
+ apiKey: "trt_…",
21
+ apiBaseUrl: "http://localhost:3000",
22
+ origin: "http://localhost:5173",
23
+ onLocaleChange: () => app.rerender(),
24
+ onTranslationsUpdated: () => app.rerender(),
25
+ });
26
+
27
+ console.log(t("common.greeting"));
28
+ console.log(t("common.welcome", { name: "Ada" }));
29
+ console.log(t("items.count", { count: 3 }));
30
+
31
+ await setLocale("fr");
32
+ ```
33
+
34
+ `init` fetches the manifest and active locale bundle using the runtime API key. Bundle URLs from the manifest are resolved against `apiBaseUrl`.
35
+
36
+ ### MAU reporting (optional)
37
+
38
+ ```ts
39
+ await init({
40
+ // …same as above
41
+ reportUsage: true, // POST /v1/usage/mau after init (SHA-256 of anonymous localStorage id)
42
+ });
43
+ ```
44
+
45
+ Enable when your plan includes MAU metering. No PII is sent — only a stable hash per browser per calendar month.
46
+
47
+ `t(key, values)` supports ICU MessageFormat (plural, select) and `{name}` interpolation via `intl-messageformat`. Configure missing-key behavior with `missingKeyFallback` (`key`, `default_message`, or `empty`) and optional `defaultMessages` at init.
48
+
49
+ See [examples/vanilla](../../examples/vanilla/) for a runnable demo.
50
+
51
+ ### Staging vs production
52
+
53
+ Pass `environment: "staging"` or `environment: "production"` in `init()`. The manifest and bundle URLs are scoped per environment (`/bundles/{projectId}/staging/…` vs `…/production/…`). Use **staging** for pre-release copy and overlay edit mode; use **production** for live customer traffic after promote + publish in admin.
54
+
55
+ Overlay edit mode is intended for **staging** only unless your organization explicitly enables production overlay (see platform `docs/CONTEXT.md` D-014).
56
+
57
+ ## Edit mode (staging overlay)
58
+
59
+ ### Recommended: edit launcher (M2-SDK-05)
60
+
61
+ Arm with `?translation_edit=1` only — a **Translate** FAB appears (staging). Users sign in, then overlay starts on click. JWT is stored in `sessionStorage`, not left in the query string.
62
+
63
+ ```ts
64
+ await init({
65
+ /* … */
66
+ environment: "staging",
67
+ editLauncher: {
68
+ signInUrl: "https://admin.example.com/overlay-grant", // redirect auth
69
+ // or requestEditToken: async () => { … } // demo/local only
70
+ },
71
+ });
72
+ ```
73
+
74
+ Optional shared secret: `?translation_edit=1&translation_edit_key=<armingKey>` when `editLauncher.armingKey` is set.
75
+
76
+ After SSO, redirect back with `#edit_token=<jwt>`; the SDK persists it and strips the hash from the URL.
77
+
78
+ ### Magic link (auto-enable)
79
+
80
+ `?translation_edit=1&edit_token=<jwt>` still auto-enables overlay on load (QA links).
81
+
82
+ ### Programmatic
83
+
84
+ ```ts
85
+ await init({ /* … */ editToken: "<jwt>" });
86
+ await enableEditMode();
87
+ disableEditMode();
88
+ ```
89
+
90
+ The overlay UI, launcher, and WebSocket client load in **separate chunks** (not in the default `import` bundle).
91
+
92
+ ### Key resolution (edit mode)
93
+
94
+ Mark translatable nodes with `data-i18n-key="your.key"` or register a DOM resolver:
95
+
96
+ ```ts
97
+ import { registerResolver } from "@stringpush/sdk";
98
+
99
+ registerResolver((element) => (element.id === "title" ? "home.title" : null));
100
+ ```
101
+
102
+ Or mark nodes with `data-i18n-key="your.key"` (see exported constant `I18N_KEY_ATTR`).
103
+
104
+ In edit mode, hovering or focusing a resolved element shows a highlight and the key path. **Click** a highlighted element to open the side panel and edit values for all project locales (saved via `PATCH /v1/overlay/values` with the edit JWT). After a successful save, the in-memory catalog for the active locale updates and `onTranslationsUpdated` runs.
105
+
106
+ If another editor changed the same value, the panel shows a **409 conflict** banner with a reload option.
107
+
108
+ While edit mode is active, the overlay WebSocket applies `translation.updated` (same environment, with version guards) to the in-memory catalog, refreshes an open edit panel for the same key, and refetches the manifest when a newer `bundle.published` arrives for the active locale — all invoke `onTranslationsUpdated`. The client reconnects automatically if the socket drops.
109
+
110
+ ## Security notes
111
+
112
+ - **Runtime key** (`trt_…`): read-only; safe in customer frontends; restrict via application **allowed domains**.
113
+ - **Edit token**: short-lived; overlay/write only; never commit or ship in production bundles.
114
+ - **Environment**: use `staging` for overlay QA; production edit requires the organization to enable **production overlay** in admin Settings (Business+), or the API env `EDIT_ON_PRODUCTION_ALLOWED=true` for local ops.
115
+ - **Origin**: set `origin` in `init()` to match an allowlisted domain (defaults to `window.location.origin`).
116
+
117
+ ## Troubleshooting
118
+
119
+ | Symptom | What to check |
120
+ |---------|----------------|
121
+ | Manifest `401` / `403` | Runtime API key; `Origin` header vs allowlisted domains |
122
+ | Overlay `403` | Page `Origin` not in application allowlist |
123
+ | `edit token required` | Pass `editToken`, or `?translation_edit=1&edit_token=` |
124
+ | `edit token is malformed` | JWT truncated in URL — use `encodeURIComponent` |
125
+ | `environment does not match` | `init({ environment })` must match the edit session environment |
126
+ | `401` on overlay save | Edit JWT expired — re-issue via `POST /v1/auth/edit-session` |
127
+ | `409` on save | Another editor saved first — use Reload in the panel |
128
+ | Live updates missing | Realtime gateway + `REDIS_URL`; same project/environment |
129
+ | CORS errors on manifest or overlay | API must allow your origin; add hostname to application **allowed domains** |
130
+ | CORS errors on bundle CDN | Hosted CDN allows `*`; SDK omits runtime key on cross-origin bundle URLs. BYO CDN: set `Access-Control-Allow-Origin: *` (or your origins) on bundle GET |
131
+
132
+ Full staging setup: [docs/integration/overlay-staging.md](../../docs/integration/overlay-staging.md).
133
+
134
+ See [docs/big-picture.md](../../docs/big-picture.md) for the full integration model.
@@ -0,0 +1,153 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/edit-token.ts
2
+ var EDIT_TOKEN_QUERY_PARAM = "edit_token";
3
+ var EDIT_MODE_QUERY_PARAM = "translation_edit";
4
+ var EDIT_LAUNCHER_KEY_QUERY_PARAM = "translation_edit_key";
5
+ var EDIT_TOKEN_STORAGE_KEY = "@translation/edit-token";
6
+ function isEditModeQueryEnabled() {
7
+ if (typeof globalThis === "undefined" || !("location" in globalThis)) {
8
+ return false;
9
+ }
10
+ const params = new URLSearchParams(
11
+ globalThis.location.search
12
+ );
13
+ const value = params.get(EDIT_MODE_QUERY_PARAM);
14
+ return value === "1" || value === "true";
15
+ }
16
+ function readEditTokenFromQuery() {
17
+ if (typeof globalThis === "undefined" || !("location" in globalThis)) {
18
+ return null;
19
+ }
20
+ const params = new URLSearchParams(
21
+ globalThis.location.search
22
+ );
23
+ return params.get(EDIT_TOKEN_QUERY_PARAM);
24
+ }
25
+ function readEditTokenFromHash() {
26
+ if (typeof globalThis === "undefined" || !("location" in globalThis)) {
27
+ return null;
28
+ }
29
+ const hash = globalThis.location.hash;
30
+ if (!hash || hash.length <= 1) {
31
+ return null;
32
+ }
33
+ const params = new URLSearchParams(hash.startsWith("#") ? hash.slice(1) : hash);
34
+ return params.get(EDIT_TOKEN_QUERY_PARAM);
35
+ }
36
+ function isEditLauncherArmed(launcher) {
37
+ if (!isEditModeQueryEnabled()) {
38
+ return false;
39
+ }
40
+ if (!_optionalChain([launcher, 'optionalAccess', _ => _.armingKey])) {
41
+ return true;
42
+ }
43
+ if (typeof globalThis === "undefined" || !("location" in globalThis)) {
44
+ return false;
45
+ }
46
+ const params = new URLSearchParams(
47
+ globalThis.location.search
48
+ );
49
+ return params.get(EDIT_LAUNCHER_KEY_QUERY_PARAM) === launcher.armingKey;
50
+ }
51
+ function readEditTokenFromStorage() {
52
+ if (typeof globalThis === "undefined" || !("sessionStorage" in globalThis)) {
53
+ return null;
54
+ }
55
+ try {
56
+ return _nullishCoalesce(globalThis.sessionStorage.getItem(
57
+ EDIT_TOKEN_STORAGE_KEY
58
+ ), () => ( null));
59
+ } catch (e) {
60
+ return null;
61
+ }
62
+ }
63
+ function persistEditToken(token) {
64
+ if (typeof globalThis === "undefined" || !("sessionStorage" in globalThis)) {
65
+ return;
66
+ }
67
+ try {
68
+ globalThis.sessionStorage.setItem(
69
+ EDIT_TOKEN_STORAGE_KEY,
70
+ token
71
+ );
72
+ } catch (e2) {
73
+ }
74
+ }
75
+ function resolveEditToken(options, override) {
76
+ const token = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_nullishCoalesce(override, () => ( options.editToken)), () => ( readEditTokenFromQuery())), () => ( readEditTokenFromHash())), () => ( readEditTokenFromStorage()));
77
+ if (token) {
78
+ persistEditToken(token);
79
+ }
80
+ return token;
81
+ }
82
+ function stripEditTokenFromUrl() {
83
+ if (typeof globalThis === "undefined" || !("location" in globalThis)) {
84
+ return;
85
+ }
86
+ const win = globalThis;
87
+ const url = new URL(win.location.href);
88
+ url.searchParams.delete(EDIT_TOKEN_QUERY_PARAM);
89
+ if (url.hash) {
90
+ const hashParams = new URLSearchParams(url.hash.slice(1));
91
+ if (hashParams.has(EDIT_TOKEN_QUERY_PARAM)) {
92
+ hashParams.delete(EDIT_TOKEN_QUERY_PARAM);
93
+ const nextHash = hashParams.toString();
94
+ url.hash = nextHash ? `#${nextHash}` : "";
95
+ }
96
+ }
97
+ const next = `${url.pathname}${url.search}${url.hash}`;
98
+ win.history.replaceState(win.history.state, "", next);
99
+ }
100
+ function shouldAutoEnableEditMode(options) {
101
+ if (options.autoEnableEditFromQuery === false) {
102
+ return false;
103
+ }
104
+ return isEditModeQueryEnabled() && resolveEditToken(options) !== null;
105
+ }
106
+
107
+ // src/edit-token-decode.ts
108
+ function decodeEditTokenClaims(token) {
109
+ const parts = token.split(".");
110
+ if (parts.length < 2) {
111
+ return null;
112
+ }
113
+ try {
114
+ const payloadJson = base64UrlDecode(parts[1]);
115
+ const payload = JSON.parse(payloadJson);
116
+ const project = payload.project;
117
+ const app = payload.app;
118
+ const env = payload.env;
119
+ const envName = payload.envName;
120
+ if (typeof project !== "string" || typeof app !== "string" || typeof env !== "string" || typeof envName !== "string") {
121
+ return null;
122
+ }
123
+ return {
124
+ projectId: project,
125
+ applicationId: app,
126
+ environmentId: env,
127
+ environmentName: envName
128
+ };
129
+ } catch (e3) {
130
+ return null;
131
+ }
132
+ }
133
+ function base64UrlDecode(segment) {
134
+ const padded = segment.replace(/-/g, "+").replace(/_/g, "/");
135
+ const pad = padded.length % 4 === 0 ? "" : "=".repeat(4 - padded.length % 4);
136
+ if (typeof atob === "function") {
137
+ return atob(padded + pad);
138
+ }
139
+ return Buffer.from(padded + pad, "base64").toString("utf8");
140
+ }
141
+
142
+
143
+
144
+
145
+
146
+
147
+
148
+
149
+
150
+
151
+
152
+ exports.readEditTokenFromQuery = readEditTokenFromQuery; exports.readEditTokenFromHash = readEditTokenFromHash; exports.isEditLauncherArmed = isEditLauncherArmed; exports.readEditTokenFromStorage = readEditTokenFromStorage; exports.persistEditToken = persistEditToken; exports.resolveEditToken = resolveEditToken; exports.stripEditTokenFromUrl = stripEditTokenFromUrl; exports.shouldAutoEnableEditMode = shouldAutoEnableEditMode; exports.decodeEditTokenClaims = decodeEditTokenClaims;
153
+ //# sourceMappingURL=chunk-FROJCNV7.umd.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/mitopalov/work/translations/packages/sdk/dist/chunk-FROJCNV7.umd.cjs","../src/edit-token.ts","../src/edit-token-decode.ts"],"names":[],"mappings":"AAAA;ACKO,IAAM,uBAAA,EAAyB,YAAA;AAC/B,IAAM,sBAAA,EAAwB,kBAAA;AAC9B,IAAM,8BAAA,EAAgC,sBAAA;AACtC,IAAM,uBAAA,EAAyB,yBAAA;AAE/B,SAAS,sBAAA,CAAA,EAAkC;AAChD,EAAA,GAAA,CAAI,OAAO,WAAA,IAAe,YAAA,GAAe,CAAA,CAAE,WAAA,GAAc,UAAA,CAAA,EAAa;AACpE,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,MAAM,OAAA,EAAS,IAAI,eAAA;AAAA,IAChB,UAAA,CAA0C,QAAA,CAAS;AAAA,EACtD,CAAA;AACA,EAAA,MAAM,MAAA,EAAQ,MAAA,CAAO,GAAA,CAAI,qBAAqB,CAAA;AAC9C,EAAA,OAAO,MAAA,IAAU,IAAA,GAAO,MAAA,IAAU,MAAA;AACpC;AAEO,SAAS,sBAAA,CAAA,EAAwC;AACtD,EAAA,GAAA,CAAI,OAAO,WAAA,IAAe,YAAA,GAAe,CAAA,CAAE,WAAA,GAAc,UAAA,CAAA,EAAa;AACpE,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,MAAM,OAAA,EAAS,IAAI,eAAA;AAAA,IAChB,UAAA,CAA0C,QAAA,CAAS;AAAA,EACtD,CAAA;AACA,EAAA,OAAO,MAAA,CAAO,GAAA,CAAI,sBAAsB,CAAA;AAC1C;AAEO,SAAS,qBAAA,CAAA,EAAuC;AACrD,EAAA,GAAA,CAAI,OAAO,WAAA,IAAe,YAAA,GAAe,CAAA,CAAE,WAAA,GAAc,UAAA,CAAA,EAAa;AACpE,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,MAAM,KAAA,EAAQ,UAAA,CAA0C,QAAA,CAAS,IAAA;AACjE,EAAA,GAAA,CAAI,CAAC,KAAA,GAAQ,IAAA,CAAK,OAAA,GAAU,CAAA,EAAG;AAC7B,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,MAAM,OAAA,EAAS,IAAI,eAAA,CAAgB,IAAA,CAAK,UAAA,CAAW,GAAG,EAAA,EAAI,IAAA,CAAK,KAAA,CAAM,CAAC,EAAA,EAAI,IAAI,CAAA;AAC9E,EAAA,OAAO,MAAA,CAAO,GAAA,CAAI,sBAAsB,CAAA;AAC1C;AAEO,SAAS,mBAAA,CAAoB,QAAA,EAA4C;AAC9E,EAAA,GAAA,CAAI,CAAC,sBAAA,CAAuB,CAAA,EAAG;AAC7B,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,GAAA,CAAI,iBAAC,QAAA,2BAAU,WAAA,EAAW;AACxB,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,GAAA,CAAI,OAAO,WAAA,IAAe,YAAA,GAAe,CAAA,CAAE,WAAA,GAAc,UAAA,CAAA,EAAa;AACpE,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,MAAM,OAAA,EAAS,IAAI,eAAA;AAAA,IAChB,UAAA,CAA0C,QAAA,CAAS;AAAA,EACtD,CAAA;AACA,EAAA,OAAO,MAAA,CAAO,GAAA,CAAI,6BAA6B,EAAA,IAAM,QAAA,CAAS,SAAA;AAChE;AAEO,SAAS,wBAAA,CAAA,EAA0C;AACxD,EAAA,GAAA,CAAI,OAAO,WAAA,IAAe,YAAA,GAAe,CAAA,CAAE,iBAAA,GAAoB,UAAA,CAAA,EAAa;AAC1E,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI;AACF,IAAA,wBACG,UAAA,CAA0C,cAAA,CAAe,OAAA;AAAA,MACxD;AAAA,IACF,CAAA,UAAK,MAAA;AAAA,EAET,EAAA,UAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEO,SAAS,gBAAA,CAAiB,KAAA,EAAqB;AACpD,EAAA,GAAA,CAAI,OAAO,WAAA,IAAe,YAAA,GAAe,CAAA,CAAE,iBAAA,GAAoB,UAAA,CAAA,EAAa;AAC1E,IAAA,MAAA;AAAA,EACF;AACA,EAAA,IAAI;AACF,IAAC,UAAA,CAA0C,cAAA,CAAe,OAAA;AAAA,MACxD,sBAAA;AAAA,MACA;AAAA,IACF,CAAA;AAAA,EACF,EAAA,WAAQ;AAAA,EAER;AACF;AAEO,SAAS,gBAAA,CACd,OAAA,EACA,QAAA,EACe;AACf,EAAA,MAAM,MAAA,sEACJ,QAAA,UACA,OAAA,CAAQ,WAAA,UACR,sBAAA,CAAuB,GAAA,UACvB,qBAAA,CAAsB,GAAA,UACtB,wBAAA,CAAyB,GAAA;AAC3B,EAAA,GAAA,CAAI,KAAA,EAAO;AACT,IAAA,gBAAA,CAAiB,KAAK,CAAA;AAAA,EACxB;AACA,EAAA,OAAO,KAAA;AACT;AAKO,SAAS,qBAAA,CAAA,EAA8B;AAC5C,EAAA,GAAA,CAAI,OAAO,WAAA,IAAe,YAAA,GAAe,CAAA,CAAE,WAAA,GAAc,UAAA,CAAA,EAAa;AACpE,IAAA,MAAA;AAAA,EACF;AACA,EAAA,MAAM,IAAA,EAAM,UAAA;AACZ,EAAA,MAAM,IAAA,EAAM,IAAI,GAAA,CAAI,GAAA,CAAI,QAAA,CAAS,IAAI,CAAA;AACrC,EAAA,GAAA,CAAI,YAAA,CAAa,MAAA,CAAO,sBAAsB,CAAA;AAC9C,EAAA,GAAA,CAAI,GAAA,CAAI,IAAA,EAAM;AACZ,IAAA,MAAM,WAAA,EAAa,IAAI,eAAA,CAAgB,GAAA,CAAI,IAAA,CAAK,KAAA,CAAM,CAAC,CAAC,CAAA;AACxD,IAAA,GAAA,CAAI,UAAA,CAAW,GAAA,CAAI,sBAAsB,CAAA,EAAG;AAC1C,MAAA,UAAA,CAAW,MAAA,CAAO,sBAAsB,CAAA;AACxC,MAAA,MAAM,SAAA,EAAW,UAAA,CAAW,QAAA,CAAS,CAAA;AACrC,MAAA,GAAA,CAAI,KAAA,EAAO,SAAA,EAAW,CAAA,CAAA,EAAI,QAAQ,CAAA,EAAA;AACpC,IAAA;AACF,EAAA;AACmC,EAAA;AACE,EAAA;AACvC;AAEyC;AAC3B,EAAA;AACH,IAAA;AACT,EAAA;AACmC,EAAA;AACrC;AD1ByC;AACA;AE9FoC;AAC9C,EAAA;AACP,EAAA;AACb,IAAA;AACT,EAAA;AAEI,EAAA;AACkC,IAAA;AACT,IAAA;AACH,IAAA;AACJ,IAAA;AACA,IAAA;AACI,IAAA;AAGtB,IAAA;AAIO,MAAA;AACT,IAAA;AACO,IAAA;AACM,MAAA;AACI,MAAA;AACA,MAAA;AACE,MAAA;AACnB,IAAA;AACM,EAAA;AACC,IAAA;AACT,EAAA;AACF;AAEkD;AACX,EAAA;AACC,EAAA;AACN,EAAA;AACN,IAAA;AAC1B,EAAA;AACiC,EAAA;AACnC;AFyFyC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/mitopalov/work/translations/packages/sdk/dist/chunk-FROJCNV7.umd.cjs","sourcesContent":[null,"/**\n * Resolves overlay edit JWT from init options, URL, or sessionStorage (M2-SDK-01).\n */\nimport type { InitOptions } from \"./types.js\";\n\nexport const EDIT_TOKEN_QUERY_PARAM = \"edit_token\";\nexport const EDIT_MODE_QUERY_PARAM = \"translation_edit\";\nexport const EDIT_LAUNCHER_KEY_QUERY_PARAM = \"translation_edit_key\";\nexport const EDIT_TOKEN_STORAGE_KEY = \"@translation/edit-token\";\n\nexport function isEditModeQueryEnabled(): boolean {\n if (typeof globalThis === \"undefined\" || !(\"location\" in globalThis)) {\n return false;\n }\n const params = new URLSearchParams(\n (globalThis as Window & typeof globalThis).location.search,\n );\n const value = params.get(EDIT_MODE_QUERY_PARAM);\n return value === \"1\" || value === \"true\";\n}\n\nexport function readEditTokenFromQuery(): string | null {\n if (typeof globalThis === \"undefined\" || !(\"location\" in globalThis)) {\n return null;\n }\n const params = new URLSearchParams(\n (globalThis as Window & typeof globalThis).location.search,\n );\n return params.get(EDIT_TOKEN_QUERY_PARAM);\n}\n\nexport function readEditTokenFromHash(): string | null {\n if (typeof globalThis === \"undefined\" || !(\"location\" in globalThis)) {\n return null;\n }\n const hash = (globalThis as Window & typeof globalThis).location.hash;\n if (!hash || hash.length <= 1) {\n return null;\n }\n const params = new URLSearchParams(hash.startsWith(\"#\") ? hash.slice(1) : hash);\n return params.get(EDIT_TOKEN_QUERY_PARAM);\n}\n\nexport function isEditLauncherArmed(launcher?: { armingKey?: string }): boolean {\n if (!isEditModeQueryEnabled()) {\n return false;\n }\n if (!launcher?.armingKey) {\n return true;\n }\n if (typeof globalThis === \"undefined\" || !(\"location\" in globalThis)) {\n return false;\n }\n const params = new URLSearchParams(\n (globalThis as Window & typeof globalThis).location.search,\n );\n return params.get(EDIT_LAUNCHER_KEY_QUERY_PARAM) === launcher.armingKey;\n}\n\nexport function readEditTokenFromStorage(): string | null {\n if (typeof globalThis === \"undefined\" || !(\"sessionStorage\" in globalThis)) {\n return null;\n }\n try {\n return (\n (globalThis as Window & typeof globalThis).sessionStorage.getItem(\n EDIT_TOKEN_STORAGE_KEY,\n ) ?? null\n );\n } catch {\n return null;\n }\n}\n\nexport function persistEditToken(token: string): void {\n if (typeof globalThis === \"undefined\" || !(\"sessionStorage\" in globalThis)) {\n return;\n }\n try {\n (globalThis as Window & typeof globalThis).sessionStorage.setItem(\n EDIT_TOKEN_STORAGE_KEY,\n token,\n );\n } catch {\n // Intent: private mode or blocked storage — overlay still works for this page load.\n }\n}\n\nexport function resolveEditToken(\n options: InitOptions,\n override?: string,\n): string | null {\n const token =\n override ??\n options.editToken ??\n readEditTokenFromQuery() ??\n readEditTokenFromHash() ??\n readEditTokenFromStorage();\n if (token) {\n persistEditToken(token);\n }\n return token;\n}\n\n/**\n * Removes edit JWT from the URL after persisting to sessionStorage (M2-SDK-05).\n */\nexport function stripEditTokenFromUrl(): void {\n if (typeof globalThis === \"undefined\" || !(\"location\" in globalThis)) {\n return;\n }\n const win = globalThis as Window & typeof globalThis;\n const url = new URL(win.location.href);\n url.searchParams.delete(EDIT_TOKEN_QUERY_PARAM);\n if (url.hash) {\n const hashParams = new URLSearchParams(url.hash.slice(1));\n if (hashParams.has(EDIT_TOKEN_QUERY_PARAM)) {\n hashParams.delete(EDIT_TOKEN_QUERY_PARAM);\n const nextHash = hashParams.toString();\n url.hash = nextHash ? `#${nextHash}` : \"\";\n }\n }\n const next = `${url.pathname}${url.search}${url.hash}`;\n win.history.replaceState(win.history.state, \"\", next);\n}\n\nexport function shouldAutoEnableEditMode(options: InitOptions): boolean {\n if (options.autoEnableEditFromQuery === false) {\n return false;\n }\n return isEditModeQueryEnabled() && resolveEditToken(options) !== null;\n}\n","/**\n * Reads edit JWT claims without verification (M2-SDK-04).\n *\n * Intent: token was already issued by our API; overlay uses claims for env scoping only.\n */\nexport type EditTokenClaims = {\n projectId: string;\n applicationId: string;\n environmentId: string;\n environmentName: string;\n};\n\nexport function decodeEditTokenClaims(token: string): EditTokenClaims | null {\n const parts = token.split(\".\");\n if (parts.length < 2) {\n return null;\n }\n\n try {\n const payloadJson = base64UrlDecode(parts[1]!);\n const payload = JSON.parse(payloadJson) as Record<string, unknown>;\n const project = payload.project;\n const app = payload.app;\n const env = payload.env;\n const envName = payload.envName;\n if (\n typeof project !== \"string\" ||\n typeof app !== \"string\" ||\n typeof env !== \"string\" ||\n typeof envName !== \"string\"\n ) {\n return null;\n }\n return {\n projectId: project,\n applicationId: app,\n environmentId: env,\n environmentName: envName,\n };\n } catch {\n return null;\n }\n}\n\nfunction base64UrlDecode(segment: string): string {\n const padded = segment.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const pad = padded.length % 4 === 0 ? \"\" : \"=\".repeat(4 - (padded.length % 4));\n if (typeof atob === \"function\") {\n return atob(padded + pad);\n }\n return Buffer.from(padded + pad, \"base64\").toString(\"utf8\");\n}\n"]}
@@ -0,0 +1,153 @@
1
+ // src/edit-token.ts
2
+ var EDIT_TOKEN_QUERY_PARAM = "edit_token";
3
+ var EDIT_MODE_QUERY_PARAM = "translation_edit";
4
+ var EDIT_LAUNCHER_KEY_QUERY_PARAM = "translation_edit_key";
5
+ var EDIT_TOKEN_STORAGE_KEY = "@translation/edit-token";
6
+ function isEditModeQueryEnabled() {
7
+ if (typeof globalThis === "undefined" || !("location" in globalThis)) {
8
+ return false;
9
+ }
10
+ const params = new URLSearchParams(
11
+ globalThis.location.search
12
+ );
13
+ const value = params.get(EDIT_MODE_QUERY_PARAM);
14
+ return value === "1" || value === "true";
15
+ }
16
+ function readEditTokenFromQuery() {
17
+ if (typeof globalThis === "undefined" || !("location" in globalThis)) {
18
+ return null;
19
+ }
20
+ const params = new URLSearchParams(
21
+ globalThis.location.search
22
+ );
23
+ return params.get(EDIT_TOKEN_QUERY_PARAM);
24
+ }
25
+ function readEditTokenFromHash() {
26
+ if (typeof globalThis === "undefined" || !("location" in globalThis)) {
27
+ return null;
28
+ }
29
+ const hash = globalThis.location.hash;
30
+ if (!hash || hash.length <= 1) {
31
+ return null;
32
+ }
33
+ const params = new URLSearchParams(hash.startsWith("#") ? hash.slice(1) : hash);
34
+ return params.get(EDIT_TOKEN_QUERY_PARAM);
35
+ }
36
+ function isEditLauncherArmed(launcher) {
37
+ if (!isEditModeQueryEnabled()) {
38
+ return false;
39
+ }
40
+ if (!launcher?.armingKey) {
41
+ return true;
42
+ }
43
+ if (typeof globalThis === "undefined" || !("location" in globalThis)) {
44
+ return false;
45
+ }
46
+ const params = new URLSearchParams(
47
+ globalThis.location.search
48
+ );
49
+ return params.get(EDIT_LAUNCHER_KEY_QUERY_PARAM) === launcher.armingKey;
50
+ }
51
+ function readEditTokenFromStorage() {
52
+ if (typeof globalThis === "undefined" || !("sessionStorage" in globalThis)) {
53
+ return null;
54
+ }
55
+ try {
56
+ return globalThis.sessionStorage.getItem(
57
+ EDIT_TOKEN_STORAGE_KEY
58
+ ) ?? null;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+ function persistEditToken(token) {
64
+ if (typeof globalThis === "undefined" || !("sessionStorage" in globalThis)) {
65
+ return;
66
+ }
67
+ try {
68
+ globalThis.sessionStorage.setItem(
69
+ EDIT_TOKEN_STORAGE_KEY,
70
+ token
71
+ );
72
+ } catch {
73
+ }
74
+ }
75
+ function resolveEditToken(options, override) {
76
+ const token = override ?? options.editToken ?? readEditTokenFromQuery() ?? readEditTokenFromHash() ?? readEditTokenFromStorage();
77
+ if (token) {
78
+ persistEditToken(token);
79
+ }
80
+ return token;
81
+ }
82
+ function stripEditTokenFromUrl() {
83
+ if (typeof globalThis === "undefined" || !("location" in globalThis)) {
84
+ return;
85
+ }
86
+ const win = globalThis;
87
+ const url = new URL(win.location.href);
88
+ url.searchParams.delete(EDIT_TOKEN_QUERY_PARAM);
89
+ if (url.hash) {
90
+ const hashParams = new URLSearchParams(url.hash.slice(1));
91
+ if (hashParams.has(EDIT_TOKEN_QUERY_PARAM)) {
92
+ hashParams.delete(EDIT_TOKEN_QUERY_PARAM);
93
+ const nextHash = hashParams.toString();
94
+ url.hash = nextHash ? `#${nextHash}` : "";
95
+ }
96
+ }
97
+ const next = `${url.pathname}${url.search}${url.hash}`;
98
+ win.history.replaceState(win.history.state, "", next);
99
+ }
100
+ function shouldAutoEnableEditMode(options) {
101
+ if (options.autoEnableEditFromQuery === false) {
102
+ return false;
103
+ }
104
+ return isEditModeQueryEnabled() && resolveEditToken(options) !== null;
105
+ }
106
+
107
+ // src/edit-token-decode.ts
108
+ function decodeEditTokenClaims(token) {
109
+ const parts = token.split(".");
110
+ if (parts.length < 2) {
111
+ return null;
112
+ }
113
+ try {
114
+ const payloadJson = base64UrlDecode(parts[1]);
115
+ const payload = JSON.parse(payloadJson);
116
+ const project = payload.project;
117
+ const app = payload.app;
118
+ const env = payload.env;
119
+ const envName = payload.envName;
120
+ if (typeof project !== "string" || typeof app !== "string" || typeof env !== "string" || typeof envName !== "string") {
121
+ return null;
122
+ }
123
+ return {
124
+ projectId: project,
125
+ applicationId: app,
126
+ environmentId: env,
127
+ environmentName: envName
128
+ };
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+ function base64UrlDecode(segment) {
134
+ const padded = segment.replace(/-/g, "+").replace(/_/g, "/");
135
+ const pad = padded.length % 4 === 0 ? "" : "=".repeat(4 - padded.length % 4);
136
+ if (typeof atob === "function") {
137
+ return atob(padded + pad);
138
+ }
139
+ return Buffer.from(padded + pad, "base64").toString("utf8");
140
+ }
141
+
142
+ export {
143
+ readEditTokenFromQuery,
144
+ readEditTokenFromHash,
145
+ isEditLauncherArmed,
146
+ readEditTokenFromStorage,
147
+ persistEditToken,
148
+ resolveEditToken,
149
+ stripEditTokenFromUrl,
150
+ shouldAutoEnableEditMode,
151
+ decodeEditTokenClaims
152
+ };
153
+ //# sourceMappingURL=chunk-X3WTVBZ6.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/edit-token.ts","../src/edit-token-decode.ts"],"sourcesContent":["/**\n * Resolves overlay edit JWT from init options, URL, or sessionStorage (M2-SDK-01).\n */\nimport type { InitOptions } from \"./types.js\";\n\nexport const EDIT_TOKEN_QUERY_PARAM = \"edit_token\";\nexport const EDIT_MODE_QUERY_PARAM = \"translation_edit\";\nexport const EDIT_LAUNCHER_KEY_QUERY_PARAM = \"translation_edit_key\";\nexport const EDIT_TOKEN_STORAGE_KEY = \"@translation/edit-token\";\n\nexport function isEditModeQueryEnabled(): boolean {\n if (typeof globalThis === \"undefined\" || !(\"location\" in globalThis)) {\n return false;\n }\n const params = new URLSearchParams(\n (globalThis as Window & typeof globalThis).location.search,\n );\n const value = params.get(EDIT_MODE_QUERY_PARAM);\n return value === \"1\" || value === \"true\";\n}\n\nexport function readEditTokenFromQuery(): string | null {\n if (typeof globalThis === \"undefined\" || !(\"location\" in globalThis)) {\n return null;\n }\n const params = new URLSearchParams(\n (globalThis as Window & typeof globalThis).location.search,\n );\n return params.get(EDIT_TOKEN_QUERY_PARAM);\n}\n\nexport function readEditTokenFromHash(): string | null {\n if (typeof globalThis === \"undefined\" || !(\"location\" in globalThis)) {\n return null;\n }\n const hash = (globalThis as Window & typeof globalThis).location.hash;\n if (!hash || hash.length <= 1) {\n return null;\n }\n const params = new URLSearchParams(hash.startsWith(\"#\") ? hash.slice(1) : hash);\n return params.get(EDIT_TOKEN_QUERY_PARAM);\n}\n\nexport function isEditLauncherArmed(launcher?: { armingKey?: string }): boolean {\n if (!isEditModeQueryEnabled()) {\n return false;\n }\n if (!launcher?.armingKey) {\n return true;\n }\n if (typeof globalThis === \"undefined\" || !(\"location\" in globalThis)) {\n return false;\n }\n const params = new URLSearchParams(\n (globalThis as Window & typeof globalThis).location.search,\n );\n return params.get(EDIT_LAUNCHER_KEY_QUERY_PARAM) === launcher.armingKey;\n}\n\nexport function readEditTokenFromStorage(): string | null {\n if (typeof globalThis === \"undefined\" || !(\"sessionStorage\" in globalThis)) {\n return null;\n }\n try {\n return (\n (globalThis as Window & typeof globalThis).sessionStorage.getItem(\n EDIT_TOKEN_STORAGE_KEY,\n ) ?? null\n );\n } catch {\n return null;\n }\n}\n\nexport function persistEditToken(token: string): void {\n if (typeof globalThis === \"undefined\" || !(\"sessionStorage\" in globalThis)) {\n return;\n }\n try {\n (globalThis as Window & typeof globalThis).sessionStorage.setItem(\n EDIT_TOKEN_STORAGE_KEY,\n token,\n );\n } catch {\n // Intent: private mode or blocked storage — overlay still works for this page load.\n }\n}\n\nexport function resolveEditToken(\n options: InitOptions,\n override?: string,\n): string | null {\n const token =\n override ??\n options.editToken ??\n readEditTokenFromQuery() ??\n readEditTokenFromHash() ??\n readEditTokenFromStorage();\n if (token) {\n persistEditToken(token);\n }\n return token;\n}\n\n/**\n * Removes edit JWT from the URL after persisting to sessionStorage (M2-SDK-05).\n */\nexport function stripEditTokenFromUrl(): void {\n if (typeof globalThis === \"undefined\" || !(\"location\" in globalThis)) {\n return;\n }\n const win = globalThis as Window & typeof globalThis;\n const url = new URL(win.location.href);\n url.searchParams.delete(EDIT_TOKEN_QUERY_PARAM);\n if (url.hash) {\n const hashParams = new URLSearchParams(url.hash.slice(1));\n if (hashParams.has(EDIT_TOKEN_QUERY_PARAM)) {\n hashParams.delete(EDIT_TOKEN_QUERY_PARAM);\n const nextHash = hashParams.toString();\n url.hash = nextHash ? `#${nextHash}` : \"\";\n }\n }\n const next = `${url.pathname}${url.search}${url.hash}`;\n win.history.replaceState(win.history.state, \"\", next);\n}\n\nexport function shouldAutoEnableEditMode(options: InitOptions): boolean {\n if (options.autoEnableEditFromQuery === false) {\n return false;\n }\n return isEditModeQueryEnabled() && resolveEditToken(options) !== null;\n}\n","/**\n * Reads edit JWT claims without verification (M2-SDK-04).\n *\n * Intent: token was already issued by our API; overlay uses claims for env scoping only.\n */\nexport type EditTokenClaims = {\n projectId: string;\n applicationId: string;\n environmentId: string;\n environmentName: string;\n};\n\nexport function decodeEditTokenClaims(token: string): EditTokenClaims | null {\n const parts = token.split(\".\");\n if (parts.length < 2) {\n return null;\n }\n\n try {\n const payloadJson = base64UrlDecode(parts[1]!);\n const payload = JSON.parse(payloadJson) as Record<string, unknown>;\n const project = payload.project;\n const app = payload.app;\n const env = payload.env;\n const envName = payload.envName;\n if (\n typeof project !== \"string\" ||\n typeof app !== \"string\" ||\n typeof env !== \"string\" ||\n typeof envName !== \"string\"\n ) {\n return null;\n }\n return {\n projectId: project,\n applicationId: app,\n environmentId: env,\n environmentName: envName,\n };\n } catch {\n return null;\n }\n}\n\nfunction base64UrlDecode(segment: string): string {\n const padded = segment.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const pad = padded.length % 4 === 0 ? \"\" : \"=\".repeat(4 - (padded.length % 4));\n if (typeof atob === \"function\") {\n return atob(padded + pad);\n }\n return Buffer.from(padded + pad, \"base64\").toString(\"utf8\");\n}\n"],"mappings":"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","names":[]}