@geniee-inc/sfa-kit 1.16.0 → 1.18.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.
Files changed (39) hide show
  1. package/GUIDE.md +92 -2
  2. package/package.json +19 -2
  3. package/src/bin/sfa-kit-bundle.d.ts +13 -0
  4. package/src/bin/sfa-kit-bundle.js +142 -0
  5. package/src/bundle.d.ts +26 -0
  6. package/src/bundle.js +1 -0
  7. package/src/index.d.ts +1 -1
  8. package/src/lib/auth/resolve-api-base.d.ts +6 -4
  9. package/src/lib/auth/resolve-api-base.js +24 -5
  10. package/src/lib/auth/useEmbedSession.d.ts +2 -1
  11. package/src/lib/auth/useEmbedSession.js +196 -26
  12. package/src/lib/bundle/build-static-bundle.d.ts +45 -0
  13. package/src/lib/bundle/build-static-bundle.js +263 -0
  14. package/src/lib/bundle/esbuild-loader.d.ts +25 -0
  15. package/src/lib/bundle/esbuild-loader.js +27 -0
  16. package/src/lib/bundle/index.d.ts +2 -0
  17. package/src/lib/bundle/index.js +1 -0
  18. package/src/lib/bundle/request-signature.d.ts +22 -0
  19. package/src/lib/bundle/request-signature.js +81 -0
  20. package/src/lib/provider/SfaClient.d.ts +13 -0
  21. package/src/lib/provider/SfaClient.js +25 -8
  22. package/src/lib/provider/SfaProvider.d.ts +2 -2
  23. package/src/lib/provider/SfaProvider.js +1 -1
  24. package/src/lib/provider/types.d.ts +12 -1
  25. package/src/lib/signing/compute-content-hash.d.ts +1 -1
  26. package/src/lib/signing/compute-signature.d.ts +17 -0
  27. package/src/lib/signing/compute-signature.js +17 -0
  28. package/src/lib/signing/create-manifest-from-directory.d.ts +13 -3
  29. package/src/lib/signing/create-manifest-from-directory.js +15 -28
  30. package/src/lib/signing/create-manifest.d.ts +7 -1
  31. package/src/lib/signing/create-manifest.js +8 -2
  32. package/src/lib/signing/index.d.ts +6 -5
  33. package/src/lib/signing/index.js +5 -4
  34. package/src/lib/signing/read-bundle-files.d.ts +19 -0
  35. package/src/lib/signing/read-bundle-files.js +41 -0
  36. package/src/lib/version.d.ts +1 -1
  37. package/src/lib/version.js +1 -1
  38. package/src/signing.d.ts +9 -4
  39. package/src/signing.js +1 -1
package/GUIDE.md CHANGED
@@ -21,7 +21,97 @@ import pkg from '@geniee-inc/sfa-kit/package.json';
21
21
  // pkg.version -> '1.14.0'
22
22
  ```
23
23
 
24
- Two additional subpaths are available: `@geniee-inc/sfa-kit/signing` exposes bundle-signing utilities (`computeContentHash`, `computeSignature`, `createManifest`, `createManifestFromDirectory`) for Node.js environments, and `./package.json` exposes the manifest. Internal modules cannot be imported directly.
24
+ Three additional subpaths are available: `@geniee-inc/sfa-kit/signing` exposes bundle-manifest utilities, `@geniee-inc/sfa-kit/bundle` builds and signs a static bundle in one call (both Node.js-only), and `./package.json` exposes the manifest. Internal modules cannot be imported directly.
25
+
26
+ ### Static bundle manifest (custom-tab uploads)
27
+
28
+ A page uploaded through GENIEE's SFA/CRM settings UI as a custom tab is a framework-free static bundle — `index.html` + `manifest.json` + `assets/` — not a Next.js app. Its `manifest.json` carries the three fields Keystone's `BundleSignatureVerifier` reads: `studio_version`, `content_hash`, `signature`.
29
+
30
+ **The signing key lives only in Keystone, so a page build never computes the signature itself.** It hashes the bundle locally and asks Keystone to sign:
31
+
32
+ ```ts
33
+ import {
34
+ readBundleFiles,
35
+ computeContentHash,
36
+ } from '@geniee-inc/sfa-kit/signing';
37
+ import type { BundleManifest } from '@geniee-inc/sfa-kit/signing';
38
+
39
+ const files = await readBundleFiles('dist');
40
+ const contentHash = computeContentHash(files);
41
+
42
+ // Keystone holds the signing key, so ask it for the signature. The Bearer token
43
+ // is the same one the rest of the API is called with.
44
+ const response = await fetch(`${apiBase}/v1/pages/sign`, {
45
+ method: 'POST',
46
+ headers: {
47
+ 'Content-Type': 'application/json',
48
+ Authorization: `Bearer ${accessToken}`,
49
+ },
50
+ body: JSON.stringify({
51
+ studio_version: studioVersion,
52
+ content_hash: contentHash,
53
+ }),
54
+ });
55
+ if (!response.ok) throw new Error(`Signing failed: ${response.status}`);
56
+ const { signature } = (await response.json()) as { signature: string };
57
+
58
+ const manifest: BundleManifest = {
59
+ studio_version: studioVersion, // the Studio build version, NOT the sfa-kit npm version
60
+ content_hash: contentHash,
61
+ signature,
62
+ };
63
+ ```
64
+
65
+ ### Building the bundle
66
+
67
+ `@geniee-inc/sfa-kit/bundle` does the whole build in one call: it bundles the entry module with esbuild, writes `index.html`, hashes the result, gets the signature from Keystone and writes `manifest.json`.
68
+
69
+ ```ts
70
+ import { buildStaticBundle } from '@geniee-inc/sfa-kit/bundle';
71
+
72
+ await buildStaticBundle({
73
+ entry: 'entry.tsx', // renders <SfaLoginGate>…</SfaLoginGate> into #root
74
+ outDir: 'dist',
75
+ studioVersion: '0.5.0', // the Studio build version, NOT the sfa-kit npm version
76
+ apiBase: 'https://keystone-api-staging.chikyu.mobi',
77
+ accessToken: token,
78
+ });
79
+ ```
80
+
81
+ Or from the command line, which is the shorter path when a build script drives it:
82
+
83
+ ```bash
84
+ SFA_KIT_ACCESS_TOKEN=<token> npx sfa-kit-bundle \
85
+ --entry entry.tsx \
86
+ --out dist \
87
+ --studio-version 0.5.0 \
88
+ --api-base https://keystone-api-staging.chikyu.mobi
89
+ ```
90
+
91
+ The token comes from the environment rather than a flag, so it stays out of shell history and the process list.
92
+
93
+ Then zip the **contents** of `dist/` — the bundle's files have to sit at the zip root, not inside a wrapper folder. Zipping is left to the caller: Node ships no zip writer, and every caller already has one.
94
+
95
+ `esbuild` is an optional peer dependency; install it alongside the kit. Its options are fixed: JS, JSX, TS and CSS bundle, but there is no loader for images or fonts, so a page importing a `.png`, `.svg` or a font — including through `url()` in CSS — fails to build. Inline such assets as data URIs, or reference them by absolute URL.
96
+ Nothing is written when signing fails, so a failed build never leaves an unsigned bundle behind to be uploaded by mistake.
97
+
98
+ `studioVersion` has no default on purpose. The signature covers `"{studio_version}:{content_hash}"` and Keystone reads the field back separately, so a guessed value produces a valid signature over the wrong version.
99
+
100
+ ### Signing exports
101
+
102
+ The lower-level pieces `buildStaticBundle` uses, for a caller that builds its own way:
103
+
104
+ | Export | Needs the signing key |
105
+ | ----------------------------- | --------------------- |
106
+ | `readBundleFiles` | No |
107
+ | `computeContentHash` | No |
108
+ | `computeSignature` | **Yes** |
109
+ | `createManifest` | **Yes** |
110
+ | `createManifestFromDirectory` | **Yes** |
111
+
112
+ The three key-taking exports exist for the service that holds the key. Calling them from a page build means inventing a key, which Keystone will not accept — and the mistake is invisible at first, because bundle verification ships in `disabled` mode: the upload succeeds, and only starts failing once the mode is switched to `enforced`.
113
+
114
+ `content_hash` must come from `computeContentHash`, never from hashing files back to back. The algorithm length-prefixes each path and each file's contents before feeding them into one running SHA-256; without those prefixes `"ab" + "c"` and `"a" + "bc"` hash identically, and the result never matches Keystone's own computation.
25
115
 
26
116
  ## Architecture
27
117
 
@@ -38,7 +128,7 @@ Every page uses `SfaLoginGate` as the outermost wrapper. It auto-detects the hos
38
128
 
39
129
  ### Props
40
130
 
41
- - **`apiBase`** (optional) — Overrides the auto-detected Keystone API endpoint. When omitted, the endpoint is resolved from a hostname: the embedding SFA/CRM host's when the page runs inside the iframe, the page's own when it runs standalone. Non-production environments (`stg.chikyu.mobi`, `dev01`-`dev09`, `devdc`, `deva0`, `dev-docker`, `dev.chikyu.local`, `localhost`, `127.0.0.1`, `[::1]`, and every Amplify host of the new frontend under `*.amplify.axiom.chikyu.mobi`: PR previews plus the `devNN.waf.` / `stg.waf.` record-detail hosts) use `https://keystone-api-staging.chikyu.mobi`; everything else (`app.chikyu.net`, any hostname not in that list) uses `https://keystone-api-production.chikyu.mobi`. Production is the fallback on purpose so a page that reaches a customer never silently talks to staging.
131
+ - **`apiBase`** (optional) — Overrides the auto-detected Keystone API endpoint. When omitted, the endpoint is resolved from a hostname: the embedding SFA/CRM host's when the page runs inside the iframe, the page's own when it runs standalone. `dev09` (`dev09.chikyu.mobi` and `dev09.waf.amplify.axiom.chikyu.mobi`) has a Keystone of its own and uses `https://keystone-api-dev09.chikyu.mobi`. The other non-production environments (`stg.chikyu.mobi`, `dev01`-`dev08`, `devdc`, `deva0`, `dev-docker`, `dev.chikyu.local`, `localhost`, `127.0.0.1`, `[::1]`, and every Amplify host of the new frontend under `*.amplify.axiom.chikyu.mobi`: PR previews plus the `devNN.waf.` / `stg.waf.` record-detail hosts) use `https://keystone-api-staging.chikyu.mobi`; everything else (`app.chikyu.net`, any hostname not in that list) uses `https://keystone-api-production.chikyu.mobi`. Production is the fallback on purpose so a page that reaches a customer never silently talks to staging.
42
132
  - **`trustedOrigin`** (optional) — The origin of the SFA/CRM host page (e.g. `"https://app.chikyu.net"`). When provided and the page runs inside an iframe, `SfaLoginGate` uses the postMessage code-exchange flow (`POST /v1/auth/exchange`) instead of a login form. Injected automatically by Keystone's content endpoint at serve time.
43
133
  - **`defaultOrganizationId`** (optional) — The org ID to auto-select when the user's account belongs to multiple organizations. **Set this to the org that generated the page's metadata** (the org used in Step 0 schema discovery). Without this prop, multi-org accounts auto-select the first available org, which may not be the intended one. Use an env var, not a literal: `defaultOrganizationId={Number(process.env.NEXT_PUBLIC_DEFAULT_ORG_ID)}`.
44
134
  - **`objectKey`** (optional) — The object this page is built for (e.g. `"companies"`). The standalone DevToolbar shows it in the page-info tooltip and in the record ID placeholder. Pass it at **both** levels; omit it only if the page genuinely has no object.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geniee-inc/sfa-kit",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "description": "React hooks and components for building pages that connect to GENIEE SFA/CRM data.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -17,8 +17,19 @@
17
17
  "import": "./src/signing.js",
18
18
  "default": "./src/signing.js"
19
19
  },
20
+ "./bundle": {
21
+ "types": "./src/bundle.d.ts",
22
+ "import": "./src/bundle.js",
23
+ "default": "./src/bundle.js"
24
+ },
20
25
  "./package.json": "./package.json"
21
26
  },
27
+ "bin": {
28
+ "sfa-kit-bundle": "./src/bin/sfa-kit-bundle.js"
29
+ },
30
+ "engines": {
31
+ "node": ">=20.1.0"
32
+ },
22
33
  "sideEffects": false,
23
34
  "files": [
24
35
  "src",
@@ -33,9 +44,15 @@
33
44
  "license": "MIT",
34
45
  "peerDependencies": {
35
46
  "react": "^18.0.0 || ^19.0.0",
36
- "react-dom": "^18.0.0 || ^19.0.0"
47
+ "react-dom": "^18.0.0 || ^19.0.0",
48
+ "esbuild": ">=0.18.0"
37
49
  },
38
50
  "dependencies": {
39
51
  "tslib": "^2.3.0"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "esbuild": {
55
+ "optional": true
56
+ }
40
57
  }
41
58
  }
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @description Runs the bundler from the command line.
4
+ *
5
+ * Exported so the test suite drives the same entry point the `bin` does, and
6
+ * so a caller embedding it can pass argv explicitly.
7
+ *
8
+ * @returns The process exit code. 0 on success, 1 on any failure.
9
+ */
10
+ export declare function runBundleCli(argv: readonly string[], env: Readonly<Record<string, string | undefined>>, out: {
11
+ log(message: string): void;
12
+ error(message: string): void;
13
+ }): Promise<number>;
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ import { __awaiter } from "tslib";
3
+ import { basename } from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ import { buildStaticBundle } from '../lib/bundle/index.js';
6
+ /** The access token is read from here, never from an argument. */
7
+ const TOKEN_ENV = 'SFA_KIT_ACCESS_TOKEN';
8
+ const USAGE = `sfa-kit-bundle — build and sign a static bundle for an SFA/CRM custom tab
9
+
10
+ Usage:
11
+ ${TOKEN_ENV}=<token> npx sfa-kit-bundle \\
12
+ --entry entry.tsx \\
13
+ --out dist \\
14
+ --studio-version 0.5.0 \\
15
+ --api-base https://keystone-api-staging.chikyu.mobi
16
+
17
+ Options:
18
+ --entry Entry module that mounts the page into #root. Required.
19
+ --out Output directory. Required.
20
+ --studio-version The Studio build version this bundle targets, not the
21
+ sfa-kit npm version. Required.
22
+ --api-base Keystone API base, without a trailing slash. Required.
23
+ --title <title> of the generated index.html. Default: SFA/CRM.
24
+ --help Show this message.
25
+
26
+ Environment:
27
+ ${TOKEN_ENV} Bearer token for Keystone, the same one the rest of the
28
+ API is called with. Required, and read from the environment rather than an
29
+ argument so it does not land in shell history or in the process list.
30
+
31
+ Writes index.html, assets/bundle.js and a signed manifest.json. Zip the
32
+ contents of the output directory — the files have to sit at the zip root.
33
+ Needs esbuild installed alongside the kit.`;
34
+ const OPTIONS = {
35
+ entry: { type: 'string' },
36
+ out: { type: 'string' },
37
+ 'studio-version': { type: 'string' },
38
+ 'api-base': { type: 'string' },
39
+ title: { type: 'string' },
40
+ help: { type: 'boolean' },
41
+ };
42
+ /**
43
+ * @description Reads the flags, reporting a malformed command line as data.
44
+ *
45
+ * NOTE: `parseArgs` throws on a misspelled flag, a flag with no value, and a
46
+ * stray positional. Left to propagate, those would reject the promise the bin
47
+ * only attaches `.then()` to, and the user would get a stack trace where the
48
+ * usage text belongs.
49
+ */
50
+ function parseCliOptions(argv) {
51
+ try {
52
+ const { values } = parseArgs({
53
+ args: [...argv],
54
+ options: OPTIONS,
55
+ allowPositionals: false,
56
+ });
57
+ return { values };
58
+ }
59
+ catch (thrown) {
60
+ return { error: thrown instanceof Error ? thrown.message : String(thrown) };
61
+ }
62
+ }
63
+ /**
64
+ * @description Runs the bundler from the command line.
65
+ *
66
+ * Exported so the test suite drives the same entry point the `bin` does, and
67
+ * so a caller embedding it can pass argv explicitly.
68
+ *
69
+ * @returns The process exit code. 0 on success, 1 on any failure.
70
+ */
71
+ export function runBundleCli(argv, env, out) {
72
+ return __awaiter(this, void 0, void 0, function* () {
73
+ var _a, _b, _c, _d, _e, _f;
74
+ const parsed = parseCliOptions(argv);
75
+ if (parsed.error != null) {
76
+ out.error(`${parsed.error}\n\n${USAGE}`);
77
+ return 1;
78
+ }
79
+ const values = parsed.values;
80
+ if (values.help) {
81
+ out.log(USAGE);
82
+ return 0;
83
+ }
84
+ const missing = ['entry', 'out', 'studio-version', 'api-base']
85
+ .filter(name => { var _a; return ((_a = values[name]) !== null && _a !== void 0 ? _a : '') === ''; })
86
+ .map(name => `--${name}`);
87
+ const accessToken = (_a = env[TOKEN_ENV]) !== null && _a !== void 0 ? _a : '';
88
+ if (accessToken === '')
89
+ missing.push(TOKEN_ENV);
90
+ if (missing.length > 0) {
91
+ out.error(`Missing required: ${missing.join(', ')}\n\n${USAGE}`);
92
+ return 1;
93
+ }
94
+ try {
95
+ const manifest = yield buildStaticBundle(Object.assign({ entry: (_b = values.entry) !== null && _b !== void 0 ? _b : '', outDir: (_c = values.out) !== null && _c !== void 0 ? _c : '', studioVersion: (_d = values['studio-version']) !== null && _d !== void 0 ? _d : '', apiBase: (_e = values['api-base']) !== null && _e !== void 0 ? _e : '', accessToken }, (((_f = values.title) !== null && _f !== void 0 ? _f : '') === '' ? {} : { title: values.title })));
96
+ out.log(`Built and signed ${values.out} ` +
97
+ `(studio_version ${manifest.studio_version}, ` +
98
+ `content_hash ${manifest.content_hash}).`);
99
+ return 0;
100
+ }
101
+ catch (thrown) {
102
+ // NOTE: buildStaticBundle removes what it wrote, so there is normally
103
+ // nothing left to upload. The one case where output survives — cleanup
104
+ // itself failing — says so in its own message.
105
+ out.error(`${thrown instanceof Error ? thrown.message : String(thrown)}\n` +
106
+ 'Nothing was signed, so nothing is ready to upload.');
107
+ return 1;
108
+ }
109
+ });
110
+ }
111
+ /** True when this module is what node was asked to run, not an import of it. */
112
+ function isEntryPoint() {
113
+ // NOTE: the file name, not a substring of the path. `includes()` would also
114
+ // fire for a consumer script under, say, ~/work/sfa-kit-bundle-demo/, which
115
+ // would start the CLI merely because that script imported runBundleCli.
116
+ //
117
+ // `import.meta.url` would be the exact comparison, but Jest transforms this
118
+ // file to CommonJS, where that syntax does not parse and the whole suite
119
+ // fails to load.
120
+ const invoked = process.argv[1];
121
+ if (invoked == null)
122
+ return false;
123
+ const name = basename(invoked);
124
+ return name === 'sfa-kit-bundle' || name === 'sfa-kit-bundle.js';
125
+ }
126
+ /* istanbul ignore next -- the process wiring is exercised by running the bin */
127
+ if (isEntryPoint()) {
128
+ // NOTE: no top-level await — the package's module target does not allow it.
129
+ void runBundleCli(process.argv.slice(2), process.env, {
130
+ log: message => console.log(message),
131
+ error: message => console.error(message),
132
+ })
133
+ .then(code => {
134
+ process.exitCode = code;
135
+ })
136
+ .catch((thrown) => {
137
+ // Nothing above should reject, but an unhandled rejection would exit 0
138
+ // on some Node versions — reporting a failed build as a success.
139
+ console.error(thrown instanceof Error ? thrown.message : String(thrown));
140
+ process.exitCode = 1;
141
+ });
142
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Static bundle builder for GENIEE SFA/CRM custom-tab uploads.
3
+ *
4
+ * This subpath export (`@geniee-inc/sfa-kit/bundle`) is Node.js-only and needs
5
+ * `esbuild` installed alongside the kit. It produces the framework-free
6
+ * `index.html` + `assets/bundle.js` + signed `manifest.json` a page is uploaded
7
+ * as; the caller zips the result.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { buildStaticBundle } from '@geniee-inc/sfa-kit/bundle';
12
+ *
13
+ * await buildStaticBundle({
14
+ * entry: 'entry.tsx',
15
+ * outDir: 'dist',
16
+ * studioVersion: '0.5.0',
17
+ * apiBase: 'https://keystone-api-staging.chikyu.mobi',
18
+ * accessToken: token,
19
+ * });
20
+ * ```
21
+ *
22
+ * @packageDocumentation
23
+ */
24
+ export type { BundleManifest } from './lib/signing/index.js';
25
+ export type { BuildStaticBundleOptions } from './lib/bundle/index.js';
26
+ export { buildStaticBundle } from './lib/bundle/index.js';
package/src/bundle.js ADDED
@@ -0,0 +1 @@
1
+ export { buildStaticBundle } from './lib/bundle/index.js';
package/src/index.d.ts CHANGED
@@ -34,7 +34,7 @@ export { ErrorBoundary } from './lib/components/ErrorBoundary';
34
34
  export { EmptyState } from './lib/components/EmptyState';
35
35
  export { FetchErrorState } from './lib/components/FetchErrorState';
36
36
  export { ErrorSnackbar } from './lib/components/ErrorSnackbar';
37
- export type { SfaAuth, SfaApiMode, SfaClientConfig, SfaRecord, SfaListResponse, SfaListParams, SfaFieldValue, SfaFieldKind, SfaFieldValueMapping, FieldGroupSpec, FieldGroupLayout, SfaFieldDescriptor, SfaFieldOption, SfaRelationDescriptor, SfaObjectDetail, SfaObjectSummary, SfaObjectCatalog, SfaObjectPermissions, SfaObjectOrigin, SfaRelationshipKind, SfaCardinality, SfaDescribeResult, SfaFieldTypeDescriptor, SfaFieldTypeCatalog, SfaFilterOperator, SfaCreateOptions, SfaFieldError, SfaBulkRowStatus, SfaBulkRowResult, SfaBulkRowError, SfaBulkUpdateRow, SfaBulkDeleteRow, } from './lib/provider/types';
37
+ export type { SfaAuth, SfaApiMode, SfaAuthErrorHandler, SfaClientConfig, SfaRecord, SfaListResponse, SfaListParams, SfaFieldValue, SfaFieldKind, SfaFieldValueMapping, FieldGroupSpec, FieldGroupLayout, SfaFieldDescriptor, SfaFieldOption, SfaRelationDescriptor, SfaObjectDetail, SfaObjectSummary, SfaObjectCatalog, SfaObjectPermissions, SfaObjectOrigin, SfaRelationshipKind, SfaCardinality, SfaDescribeResult, SfaFieldTypeDescriptor, SfaFieldTypeCatalog, SfaFilterOperator, SfaCreateOptions, SfaFieldError, SfaBulkRowStatus, SfaBulkRowResult, SfaBulkRowError, SfaBulkUpdateRow, SfaBulkDeleteRow, } from './lib/provider/types';
38
38
  export { SFA_BULK_MAX_ROWS } from './lib/provider/SfaClient';
39
39
  export type { SfaSession, AuthResult, OrganizationChoice, } from './lib/auth/types';
40
40
  export { buildRecordDetailUrl, resolveHostOrigin, } from './lib/navigation/record-detail-url';
@@ -1,12 +1,14 @@
1
1
  export declare const KEYSTONE_PRODUCTION_API_BASE = "https://keystone-api-production.chikyu.mobi";
2
2
  export declare const KEYSTONE_STAGING_API_BASE = "https://keystone-api-staging.chikyu.mobi";
3
+ export declare const KEYSTONE_DEV09_API_BASE = "https://keystone-api-dev09.chikyu.mobi";
3
4
  /**
4
5
  * @description Resolves the Keystone API base URL from an origin.
5
6
  *
6
- * Every non-production chikyu environment — `stg.chikyu.mobi`, the dev tier,
7
- * local development, and Amplify PR previews — uses the staging Keystone API.
8
- * Everything else — `app.chikyu.net`, an origin this table does not know, or
9
- * no origin at all — uses production.
7
+ * `dev09` has a keystone of its own. Every other non-production chikyu
8
+ * environment — `stg.chikyu.mobi`, the rest of the dev tier, local development,
9
+ * and Amplify PR previews — uses the staging Keystone API. Everything else —
10
+ * `app.chikyu.net`, an origin this table does not know, or no origin at all —
11
+ * uses production.
10
12
  *
11
13
  * Production is the fallback on purpose: a page that reaches a real customer
12
14
  * must never silently talk to staging. A developer on an unrecognised host
@@ -1,6 +1,10 @@
1
1
  export const KEYSTONE_PRODUCTION_API_BASE = 'https://keystone-api-production.chikyu.mobi';
2
2
  export const KEYSTONE_STAGING_API_BASE = 'https://keystone-api-staging.chikyu.mobi';
3
- /** Dev-tier chikyu (legacy frontend) hosts: dev01-dev09, devdc, deva0, and the like. */
3
+ export const KEYSTONE_DEV09_API_BASE = 'https://keystone-api-dev09.chikyu.mobi';
4
+ /**
5
+ * Dev-tier chikyu (legacy frontend) hosts: dev01-dev08, devdc, deva0, and the
6
+ * like. `dev09` matches this too but is taken by `DEV09_HOSTNAMES` first.
7
+ */
4
8
  const DEV_HOST_PATTERN = /^dev\w+\.chikyu\.mobi$/;
5
9
  /**
6
10
  * Amplify-hosted new frontend. Covers PR previews
@@ -21,13 +25,26 @@ const LOCAL_HOSTNAMES = new Set([
21
25
  ]);
22
26
  /** Shared pre-production chikyu environments. */
23
27
  const STAGING_HOSTNAMES = new Set(['stg.chikyu.mobi']);
28
+ /**
29
+ * dev09 has a keystone of its own. One keystone delegates to exactly one chikyu
30
+ * environment, so the staging keystone follows `stg` and this one follows
31
+ * `dev09`, the JapanAI Studio integration environment. Both of dev09's frontends
32
+ * are listed: the legacy chikyu host and the Amplify host that frames the page.
33
+ * Exact hostnames, because `DEV_HOST_PATTERN` and `AMPLIFY_PATTERN` below would
34
+ * otherwise claim them for staging.
35
+ */
36
+ const DEV09_HOSTNAMES = new Set([
37
+ 'dev09.chikyu.mobi',
38
+ 'dev09.waf.amplify.axiom.chikyu.mobi',
39
+ ]);
24
40
  /**
25
41
  * @description Resolves the Keystone API base URL from an origin.
26
42
  *
27
- * Every non-production chikyu environment — `stg.chikyu.mobi`, the dev tier,
28
- * local development, and Amplify PR previews — uses the staging Keystone API.
29
- * Everything else — `app.chikyu.net`, an origin this table does not know, or
30
- * no origin at all — uses production.
43
+ * `dev09` has a keystone of its own. Every other non-production chikyu
44
+ * environment — `stg.chikyu.mobi`, the rest of the dev tier, local development,
45
+ * and Amplify PR previews — uses the staging Keystone API. Everything else —
46
+ * `app.chikyu.net`, an origin this table does not know, or no origin at all —
47
+ * uses production.
31
48
  *
32
49
  * Production is the fallback on purpose: a page that reaches a real customer
33
50
  * must never silently talk to staging. A developer on an unrecognised host
@@ -41,6 +58,8 @@ export function resolveApiBase(origin) {
41
58
  const hostname = hostnameOf(origin);
42
59
  if (hostname == null)
43
60
  return KEYSTONE_PRODUCTION_API_BASE;
61
+ if (DEV09_HOSTNAMES.has(hostname))
62
+ return KEYSTONE_DEV09_API_BASE;
44
63
  const isNonProduction = STAGING_HOSTNAMES.has(hostname) ||
45
64
  LOCAL_HOSTNAMES.has(hostname) ||
46
65
  DEV_HOST_PATTERN.test(hostname) ||
@@ -1,3 +1,4 @@
1
+ import type { SfaAuthErrorHandler } from '../provider/types';
1
2
  import type { EmbedContext } from './embed-protocol';
2
3
  import type { SfaSession } from './types';
3
4
  export type EmbedPhase = {
@@ -28,7 +29,7 @@ export type EmbedPhase = {
28
29
  export interface EmbedSessionState {
29
30
  readonly phase: EmbedPhase;
30
31
  /** Passed to SfaProvider so a 401 triggers the code-request fallback. */
31
- readonly handleAuthError: () => void;
32
+ readonly handleAuthError: SfaAuthErrorHandler;
32
33
  }
33
34
  /**
34
35
  * @description Keeps an embedded page authenticated over the postMessage