@manta-eu/codegen 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,6 @@ pnpm manta-codegen generate \
12
12
  --datastore your-datastore \
13
13
  --keycloak-url https://keycloak.example \
14
14
  --keycloak-realm your-realm \
15
- --client-id your-realm-app \
16
15
  --out ./.ldkit
17
16
  ```
18
17
 
@@ -27,14 +26,33 @@ Only `namespaces.ts` holds the realm IRIs.
27
26
 
28
27
  ## Log in
29
28
 
30
- `MANTA_CLIENT_SECRET` decides the path.
29
+ `MANTA_CLIENT_SECRET` decides the path. Login itself is `@manta-eu/auth`'s job — see
30
+ [its README](https://www.npmjs.com/package/@manta-eu/auth) for how the browser login works, where
31
+ the login is stored and how long it lasts. `@manta-eu/upload` reads and writes the same store, so
32
+ one login serves both CLIs.
31
33
 
32
- **No secret — a person.** The CLI starts the OAuth device flow with PKCE. It prints a link and a
33
- code, then waits for the browser. Keycloak needs a public client that carries
34
- `oauth2.device.authorization.grant.enabled`.
34
+ **No secret — a person.** Log in once:
35
35
 
36
- **A secret — a machine.** The CLI asks for a service account token. Keycloak needs a confidential
37
- client with service account roles. Add `--scope` to ask for a narrow token.
36
+ ```shell
37
+ pnpm manta-codegen login \
38
+ --keycloak-url https://keycloak.example \
39
+ --keycloak-realm your-realm
40
+ ```
41
+
42
+ | Command | What it does |
43
+ | -------- | ------------------------------------------------------------------- |
44
+ | `login` | Logs in and stores the login. |
45
+ | `status` | Says whether a login is stored, and for whom. Exits 1 when none is. |
46
+ | `logout` | Revokes the login at Keycloak and deletes the stored copy. |
47
+
48
+ Each takes the same `--keycloak-url` and `--keycloak-realm` as `generate`, because a login belongs to
49
+ one Keycloak, realm and client. The client defaults to the realm's `<realm>-cli`, a public client
50
+ made for this, so `--client-id` is only needed for another one. `generate` logs in by itself when
51
+ nothing is stored and a terminal is attached; without one it fails and names `login` instead of
52
+ waiting for a browser.
53
+
54
+ **A secret — a machine.** Set `MANTA_CLIENT_SECRET` to the secret of the realm's `manta-codegen`
55
+ client, which the CLI then uses by default. The CLI asks for a service account token. Add `--scope` to ask for a narrow token.
38
56
 
39
57
  ```shell
40
58
  export MANTA_CLIENT_SECRET=... # never a flag: an argument list is public on the machine
@@ -42,12 +60,11 @@ export MANTA_CLIENT_SECRET=... # never a flag: an argument list is public on t
42
60
 
43
61
  Two traps:
44
62
 
45
- - `--client-id` must name the realm's **app** client. The API compares the token's `azp` against its
46
- own `VITE_KEYCLOAK_CLIENT_ID`, and answers 401 for any other client.
47
63
  - The API grants a datastore to a person, or to a service account holding the `operator` realm role.
48
- Without that role a service account token is refused the datastore, so a local run needs either the
49
- device login or a client shaped like `manta-codegen`, which has no device flow and needs
50
- `MANTA_CLIENT_SECRET`.
64
+ `manta-codegen` holds it; a service account without it is refused the datastore.
65
+ - The API can compare the token's `azp` against its own `VITE_KEYCLOAK_CLIENT_ID` and answer 401 for
66
+ any other client. That check is off today (`authorizedPartyCheckEnabled`). Turning it on 401s
67
+ both `<realm>-cli` and `manta-codegen` unless it learns to accept them.
51
68
 
52
69
  ## What the datastore does not hold
53
70
 
package/dist/cli.js CHANGED
@@ -1,23 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, Option } from "commander";
3
3
  import { resolve } from "node:path";
4
+ import { cachedToken, ciClientId, clientIdHelp, describeError, mintWithClientCredentials, publicClientFrom, registerAuthCommands, userTokenSource, } from "@manta-eu/auth";
4
5
  import { fetchRealmShapes, sparqlEndpoint } from "./fetch-shapes.js";
5
- import { pollForDeviceToken, startDeviceLogin } from "./device-login.js";
6
- import { describeError } from "./http.js";
7
6
  import { generatePackage } from "./generate.js";
8
- import { cachedToken, mintWithClientCredentials } from "./token.js";
9
7
  // The secret stays out of the flags: an argument list is readable to every process on the machine.
10
8
  function tokenSourceFor(options) {
11
- const client = { baseUrl: options.keycloakUrl, realm: options.keycloakRealm, clientId: options.clientId };
12
9
  const secret = process.env.MANTA_CLIENT_SECRET;
13
10
  if (secret) {
11
+ const client = publicClientFrom({ ...options, clientId: options.clientId ?? ciClientId });
14
12
  return cachedToken(() => mintWithClientCredentials({ ...client, clientSecret: secret, scope: options.scope }));
15
13
  }
16
- return cachedToken(async () => {
17
- const login = await startDeviceLogin(client);
18
- console.error(`manta-codegen: open ${login.verificationUri} and enter the code ${login.userCode}`);
19
- return pollForDeviceToken(client, login);
20
- });
14
+ return userTokenSource(publicClientFrom(options), { label: "manta-codegen", useDeviceCode: options.useDeviceCode });
21
15
  }
22
16
  function reportTargetClasses(targetClasses) {
23
17
  const total = Object.values(targetClasses).reduce((sum, count) => sum + count, 0);
@@ -53,16 +47,18 @@ program
53
47
  .requiredOption("--datastore <id>", "datastore to read")
54
48
  .requiredOption("--keycloak-url <url>", "Keycloak base URL")
55
49
  .requiredOption("--keycloak-realm <realm>", "Keycloak realm")
56
- .requiredOption("--client-id <id>", "Keycloak client id")
57
- .option("--scope <scope>", "OAuth scope to ask for")
50
+ .option("--client-id <id>", clientIdHelp)
51
+ .option("--scope <scope>", "OAuth scope to ask a service account token for")
52
+ .option("--use-device-code", "if a login is needed, use a one-time code instead of a browser redirect")
58
53
  .addOption(new Option("--out <dir>", "directory to write into").default("./.ldkit"))
59
54
  .action(async (options) => {
60
55
  try {
61
56
  await generate(options);
62
57
  }
63
58
  catch (error) {
64
- console.error(describeError(error));
59
+ console.error(`manta-codegen: ${describeError(error)}`);
65
60
  process.exit(1);
66
61
  }
67
62
  });
68
- program.parse();
63
+ registerAuthCommands(program, { label: "manta-codegen" });
64
+ await program.parseAsync();
@@ -1,5 +1,4 @@
1
- import type { FetchLike } from "./http.js";
2
- import type { TokenSource } from "./token.js";
1
+ import { type FetchLike, type TokenSource } from "@manta-eu/auth";
3
2
  export type RealmShapes = {
4
3
  vocabBase: string;
5
4
  ontologyGraphIri: string;
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-shapes.d.ts","sourceRoot":"","sources":["../src/fetch-shapes.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC,CAAC;AAEF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,WAAW,EAClB,SAAS,GAAE,SAAiB,GAC3B,OAAO,CAAC,WAAW,CAAC,CActB"}
1
+ {"version":3,"file":"fetch-shapes.d.ts","sourceRoot":"","sources":["../src/fetch-shapes.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAe,MAAM,gBAAgB,CAAC;AAE/E,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC,CAAC;AAEF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,WAAW,EAClB,SAAS,GAAE,SAAiB,GAC3B,OAAO,CAAC,WAAW,CAAC,CAkBtB"}
@@ -1,12 +1,13 @@
1
1
  import { countTargetClasses, fetchGraphList, fetchGraphTurtle, findOntologyGraph, shaclGraphIri, vocabBaseFromOntologyGraph, } from "./shapes.js";
2
+ import { withRenewal } from "@manta-eu/auth";
2
3
  export function sparqlEndpoint(apiUrl, datastoreId) {
3
4
  return `${apiUrl.replace(/\/$/, "")}/d/${encodeURIComponent(datastoreId)}/sparql`;
4
5
  }
5
6
  export async function fetchRealmShapes(endpoint, token, fetchImpl = fetch) {
6
- const graphs = await fetchGraphList({ endpoint, token: await token() }, fetchImpl);
7
+ const graphs = await withRenewal(token, (accessToken) => fetchGraphList({ endpoint, token: accessToken }, fetchImpl));
7
8
  const ontologyGraphIri = findOntologyGraph(graphs);
8
- const shacl = await fetchGraphTurtle({ endpoint, graphIri: shaclGraphIri, token: await token() }, fetchImpl);
9
- const ontology = await fetchGraphTurtle({ endpoint, graphIri: ontologyGraphIri, token: await token() }, fetchImpl);
9
+ const shacl = await withRenewal(token, (accessToken) => fetchGraphTurtle({ endpoint, graphIri: shaclGraphIri, token: accessToken }, fetchImpl));
10
+ const ontology = await withRenewal(token, (accessToken) => fetchGraphTurtle({ endpoint, graphIri: ontologyGraphIri, token: accessToken }, fetchImpl));
10
11
  return {
11
12
  vocabBase: vocabBaseFromOntologyGraph(ontologyGraphIri),
12
13
  ontologyGraphIri,
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"AAYA,wBAAgB,YAAY,CAAC,KAAK,GAAE,MAAqB,GAAG,MAAM,CAOjE;AAOD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEvD;AAGD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEpF;AAOD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ3E;AAMD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAU3D;AAGD,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAchE;AAED,MAAM,MAAM,YAAY,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAElF,wBAAsB,eAAe,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWjH"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"AAYA,wBAAgB,YAAY,CAAC,KAAK,GAAE,MAAqB,GAAG,MAAM,CAOjE;AAOD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEvD;AAGD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEpF;AAOD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ3E;AAMD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAQ3D;AAGD,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAchE;AAED,MAAM,MAAM,YAAY,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAElF,wBAAsB,eAAe,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWjH"}
package/dist/generate.js CHANGED
@@ -14,7 +14,7 @@ export function ldkitCliPath(entry = ldkitEntry()) {
14
14
  if (existsSync(candidate))
15
15
  return candidate;
16
16
  }
17
- throw new Error("manta-codegen: cannot find the LDkit CLI beside the resolved ldkit package.");
17
+ throw new Error("cannot find the LDkit CLI beside the resolved ldkit package.");
18
18
  }
19
19
  // LDkit writes a property name as a bare object key, and Meta sends terms such as `1dClick`.
20
20
  const objectKey = /(?<=^\s+)[^\s"':]+(?=:\s)/gm;
@@ -51,7 +51,7 @@ export function failOnUnknownNamespace(outDir) {
51
51
  if (!existsSync(path))
52
52
  return;
53
53
  const names = splitDeclarations(readFileSync(path, "utf8")).declarations.map((each) => each.name);
54
- throw new Error(`manta-codegen: the endpoint named no prefix for some terms, so LDkit wrote ${unknownFile}: ${names.join(", ")}.`);
54
+ throw new Error(`the endpoint named no prefix for some terms, so LDkit wrote ${unknownFile}: ${names.join(", ")}.`);
55
55
  }
56
56
  // The LDkit CLI reports a failure on stderr and still exits 0, so an empty directory is the signal.
57
57
  export function runLdkit(shaclPath, outDir) {
@@ -59,7 +59,7 @@ export function runLdkit(shaclPath, outDir) {
59
59
  mkdirSync(outDir, { recursive: true });
60
60
  execFileSync(process.execPath, [ldkitCliPath(), "shacl-to-package", "--prefix-alias", `${realmPrefix}=Manta`, "file", shaclPath, outDir], { stdio: "inherit" });
61
61
  if (!existsSync(join(outDir, "index.ts"))) {
62
- throw new Error(`manta-codegen: LDkit wrote no schema into ${outDir}. Read the error above.`);
62
+ throw new Error(`LDkit wrote no schema into ${outDir}. Read the error above.`);
63
63
  }
64
64
  failOnUnknownNamespace(outDir);
65
65
  }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,6 @@
1
- export { deviceLoginTokenSource, pollForDeviceToken, pollForDeviceTokens, refreshAccessToken, startDeviceLogin, type DeviceLogin, type DeviceTokens, type PkcePair, type PublicClient, type Sleep, } from "./device-login.js";
2
1
  export { fetchRealmShapes, sparqlEndpoint, type RealmShapes } from "./fetch-shapes.js";
3
2
  export { failOnUnknownNamespace, generatePackage, removeGeneratedFiles, type ShapeSources } from "./generate.js";
4
3
  export { sortSchemas } from "./sort-schemas.js";
5
- export { describeError, isTransientStatus, readJson, type FetchLike } from "./http.js";
6
4
  export { countTargetClasses } from "./shapes.js";
7
- export { cachedToken, mintWithClientCredentials, type ClientCredentials, type TokenSource } from "./token.js";
8
5
  export { mergeShapes, realmPrefix, sortQuads } from "./turtle.js";
9
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,KAAK,GACX,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAE,oBAAoB,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AACjH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,QAAQ,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AACvF,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,yBAAyB,EAAE,KAAK,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9G,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAE,oBAAoB,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AACjH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -1,8 +1,5 @@
1
- export { deviceLoginTokenSource, pollForDeviceToken, pollForDeviceTokens, refreshAccessToken, startDeviceLogin, } from "./device-login.js";
2
1
  export { fetchRealmShapes, sparqlEndpoint } from "./fetch-shapes.js";
3
2
  export { failOnUnknownNamespace, generatePackage, removeGeneratedFiles } from "./generate.js";
4
3
  export { sortSchemas } from "./sort-schemas.js";
5
- export { describeError, isTransientStatus, readJson } from "./http.js";
6
4
  export { countTargetClasses } from "./shapes.js";
7
- export { cachedToken, mintWithClientCredentials } from "./token.js";
8
5
  export { mergeShapes, realmPrefix, sortQuads } from "./turtle.js";
package/dist/shapes.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type FetchLike } from "./http.js";
1
+ import { type FetchLike } from "@manta-eu/auth";
2
2
  export declare const shaclGraphIri = "https://rdfox.com/default-base-iri/shacl";
3
3
  export declare function constructGraphQuery(graphIri: string): string;
4
4
  export declare function graphListQuery(): string;
@@ -1 +1 @@
1
- {"version":3,"file":"shapes.d.ts","sourceRoot":"","sources":["../src/shapes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAU,MAAM,WAAW,CAAC;AAGnD,eAAO,MAAM,aAAa,6CAA6C,CAAC;AAMxE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM5D;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAID,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAIxD;AAID,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAiBtE;AAED,wBAAgB,0BAA0B,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE,MAAM,MAAM,YAAY,GAAG,eAAe,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAiClE,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,GAAE,SAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ3G;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,eAAe,EAAE,SAAS,GAAE,SAAiB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAI7G;AAMD,eAAO,MAAM,UAAU,YAAY,CAAC;AAGpC,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAazE"}
1
+ {"version":3,"file":"shapes.d.ts","sourceRoot":"","sources":["../src/shapes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAA+B,MAAM,gBAAgB,CAAC;AAG7E,eAAO,MAAM,aAAa,6CAA6C,CAAC;AAMxE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM5D;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAID,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAIxD;AAID,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAiBtE;AAED,wBAAgB,0BAA0B,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE,MAAM,MAAM,YAAY,GAAG,eAAe,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAkClE,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,GAAE,SAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ3G;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,eAAe,EAAE,SAAS,GAAE,SAAiB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAI7G;AAMD,eAAO,MAAM,UAAU,YAAY,CAAC;AAGpC,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAazE"}
package/dist/shapes.js CHANGED
@@ -1,11 +1,11 @@
1
- import { postTo } from "./http.js";
1
+ import { postTo, RefusedRequestError } from "@manta-eu/auth";
2
2
  // The store puts the shapes here for every realm, so this one carries no realm identifier.
3
3
  export const shaclGraphIri = "https://rdfox.com/default-base-iri/shacl";
4
4
  const storeDefaultBaseIri = "https://rdfox.com/default-base-iri/";
5
5
  const ontologyGraphSuffix = "ontology";
6
6
  export function constructGraphQuery(graphIri) {
7
7
  if (graphIri.includes(">")) {
8
- throw new Error(`manta-codegen: the graph IRI ${graphIri} carries a ">", which cannot go into a query.`);
8
+ throw new Error(`the graph IRI ${graphIri} carries a ">", which cannot go into a query.`);
9
9
  }
10
10
  return `CONSTRUCT { ?s ?p ?o } WHERE { GRAPH <${graphIri}> { ?s ?p ?o } }`;
11
11
  }
@@ -21,11 +21,11 @@ export function readGraphList(results) {
21
21
  export function findOntologyGraph(graphIris) {
22
22
  const candidates = graphIris.filter((iri) => iri.endsWith(ontologyGraphSuffix) && !iri.startsWith(storeDefaultBaseIri));
23
23
  if (candidates.length === 0) {
24
- throw new Error(`manta-codegen: the datastore holds no ontology graph. Looked for an IRI that ends with ` +
24
+ throw new Error(`the datastore holds no ontology graph. Looked for an IRI that ends with ` +
25
25
  `"${ontologyGraphSuffix}" among: ${graphIris.join(", ")}.`);
26
26
  }
27
27
  if (candidates.length > 1) {
28
- throw new Error(`manta-codegen: the datastore holds more than one ontology graph: ${candidates.join(", ")}.`);
28
+ throw new Error(`the datastore holds more than one ontology graph: ${candidates.join(", ")}.`);
29
29
  }
30
30
  return candidates[0];
31
31
  }
@@ -45,7 +45,7 @@ async function runSparql({ endpoint, token }, { query, accept, action }, fetchIm
45
45
  function refusal(call, response, body) {
46
46
  const rejectsToken = response.status === 401 || response.status === 403;
47
47
  const hint = rejectsToken ? " Check that the token carries access to this datastore." : "";
48
- return new Error(`manta-codegen: ${call.endpoint} refused ${call.action} with status ${response.status}.${hint} ${body.trim()}`);
48
+ return new RefusedRequestError(`${call.endpoint} refused ${call.action} with status ${response.status}.${hint} ${body.trim()}`, response.status);
49
49
  }
50
50
  export async function fetchGraphTurtle(request, fetchImpl = fetch) {
51
51
  const call = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manta-eu/codegen",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "CLI that generates LDkit schemas from a realm's SHACL shapes, read from its datastore",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,9 +17,10 @@
17
17
  "dependencies": {
18
18
  "@types/n3": "^1.26.0",
19
19
  "commander": "^15.0.0",
20
- "ldkit": "2.8.0",
20
+ "ldkit": "^2.8.0",
21
21
  "n3": "^2.7.12",
22
- "prettier": "^3.8.1"
22
+ "prettier": "^3.8.1",
23
+ "@manta-eu/auth": "0.1.4"
23
24
  },
24
25
  "devDependencies": {
25
26
  "@types/node": "^24.13.3",
@@ -27,8 +28,8 @@
27
28
  "@typescript-eslint/parser": "^8.56.1",
28
29
  "eslint": "^9.39.2",
29
30
  "rimraf": "^6.1.3",
30
- "typescript": "^5.0.0",
31
- "vitest": "^3.0.0"
31
+ "typescript": "^5.9.3",
32
+ "vitest": "^4.1.8"
32
33
  },
33
34
  "main": "./dist/index.js",
34
35
  "types": "./dist/index.d.ts",
@@ -1,45 +0,0 @@
1
- import { type FetchLike } from "./http.js";
2
- import { type TokenSource } from "./token.js";
3
- export type PublicClient = {
4
- baseUrl: string;
5
- realm: string;
6
- clientId: string;
7
- };
8
- export type DeviceLogin = {
9
- deviceCode: string;
10
- userCode: string;
11
- verificationUri: string;
12
- intervalMs: number;
13
- expiresInMs: number;
14
- codeVerifier: string;
15
- };
16
- export type PkcePair = {
17
- verifier: string;
18
- challenge: string;
19
- };
20
- export type Sleep = (ms: number) => Promise<void>;
21
- export declare function deviceAuthEndpoint(baseUrl: string, realm: string): string;
22
- export declare function createPkcePair(): PkcePair;
23
- export declare function startDeviceLogin(client: PublicClient, fetchImpl?: FetchLike, pkce?: PkcePair): Promise<DeviceLogin>;
24
- export type DeviceTokens = {
25
- accessToken: string;
26
- refreshToken?: string;
27
- };
28
- export declare function pollForDeviceToken(...args: Parameters<typeof pollForDeviceTokens>): Promise<string>;
29
- export declare function pollForDeviceTokens(client: PublicClient, { deviceCode, codeVerifier, intervalMs, expiresInMs, }: Pick<DeviceLogin, "deviceCode" | "codeVerifier"> & {
30
- intervalMs?: number;
31
- expiresInMs?: number;
32
- }, { fetchImpl, sleep, clock, }?: {
33
- fetchImpl?: FetchLike;
34
- sleep?: Sleep;
35
- clock?: () => number;
36
- }): Promise<DeviceTokens>;
37
- export declare function refreshAccessToken(client: PublicClient, refreshToken: string, fetchImpl?: FetchLike): Promise<DeviceTokens | undefined>;
38
- export declare function deviceLoginTokenSource(client: PublicClient, prompt: (login: DeviceLogin) => void, { fetchImpl, login, poll, }?: {
39
- fetchImpl?: FetchLike;
40
- login?: (client: PublicClient, fetchImpl: FetchLike) => Promise<DeviceLogin>;
41
- poll?: (client: PublicClient, login: DeviceLogin, options: {
42
- fetchImpl: FetchLike;
43
- }) => Promise<DeviceTokens>;
44
- }): TokenSource;
45
- //# sourceMappingURL=device-login.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"device-login.d.ts","sourceRoot":"","sources":["../src/device-login.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAoD,MAAM,WAAW,CAAC;AAC7F,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAE7D,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/D,MAAM,MAAM,KAAK,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAclD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzE;AAGD,wBAAgB,cAAc,IAAI,QAAQ,CAIzC;AA+BD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,YAAY,EACpB,SAAS,GAAE,SAAiB,EAC5B,IAAI,GAAE,QAA2B,GAChC,OAAO,CAAC,WAAW,CAAC,CAiBtB;AAyBD,MAAM,MAAM,YAAY,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1E,wBAAsB,kBAAkB,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAEzG;AAED,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,YAAY,EACpB,EACE,UAAU,EACV,YAAY,EACZ,UAA8B,EAC9B,WAAgC,GACjC,EAAE,IAAI,CAAC,WAAW,EAAE,YAAY,GAAG,cAAc,CAAC,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,EACnG,EACE,SAAiB,EACjB,KAAgB,EAChB,KAAgB,GACjB,GAAE;IAAE,SAAS,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAA;CAAO,GACrE,OAAO,CAAC,YAAY,CAAC,CAwBvB;AAGD,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,YAAY,EACpB,YAAY,EAAE,MAAM,EACpB,SAAS,GAAE,SAAiB,GAC3B,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAiBnC;AAGD,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,EACpC,EACE,SAAiB,EACjB,KAAwB,EACxB,IAA0B,GAC3B,GAAE;IACD,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7E,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE;QAAE,SAAS,EAAE,SAAS,CAAA;KAAE,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;CAC1G,GACL,WAAW,CAkBb"}
@@ -1,127 +0,0 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- import { formRequest, isTransientStatus, postTo, readJson } from "./http.js";
3
- import { tokenEndpoint } from "./token.js";
4
- const deviceCodeGrant = "urn:ietf:params:oauth:grant-type:device_code";
5
- // RFC 8628 says to add this much whenever the server answers slow_down.
6
- const slowDownStepMs = 5_000;
7
- const defaultIntervalMs = 5_000;
8
- // RFC 8628 makes expires_in optional, and Keycloak sends ten minutes.
9
- const defaultExpiresInMs = 600_000;
10
- const sleepFor = (ms) => new Promise((done) => setTimeout(done, ms));
11
- export function deviceAuthEndpoint(baseUrl, realm) {
12
- return `${baseUrl}/realms/${realm}/protocol/openid-connect/auth/device`;
13
- }
14
- // A public client holds no secret, so PKCE is what ties the token request back to this process.
15
- export function createPkcePair() {
16
- const verifier = randomBytes(32).toString("base64url");
17
- return { verifier, challenge: createHash("sha256").update(verifier).digest("base64url") };
18
- }
19
- function readDeviceLogin(body, endpoint, codeVerifier) {
20
- const deviceCode = body.device_code;
21
- const userCode = body.user_code;
22
- const verificationUri = body.verification_uri_complete ?? body.verification_uri;
23
- if (!deviceCode || !userCode || !verificationUri) {
24
- throw new Error(`manta-codegen: the device login response from ${endpoint} is incomplete.`);
25
- }
26
- return {
27
- deviceCode,
28
- userCode,
29
- verificationUri,
30
- intervalMs: body.interval ? body.interval * 1000 : defaultIntervalMs,
31
- expiresInMs: body.expires_in ? body.expires_in * 1000 : defaultExpiresInMs,
32
- codeVerifier,
33
- };
34
- }
35
- // No secret lives on the machine: a developer needs nothing beyond a browser login.
36
- export async function startDeviceLogin(client, fetchImpl = fetch, pkce = createPkcePair()) {
37
- const endpoint = deviceAuthEndpoint(client.baseUrl, client.realm);
38
- const request = formRequest({
39
- client_id: client.clientId,
40
- code_challenge: pkce.challenge,
41
- code_challenge_method: "S256",
42
- });
43
- const response = await postTo(endpoint, request, fetchImpl);
44
- if (!response.ok) {
45
- throw new Error(`manta-codegen: ${endpoint} refused the device login with status ${response.status}. ` +
46
- `Check that the client ${client.clientId} is public and allows the OAuth device flow.`);
47
- }
48
- return readDeviceLogin(await readJson(response, endpoint), endpoint, pkce.verifier);
49
- }
50
- function deviceLoginFailure(error, status) {
51
- switch (error) {
52
- case "access_denied":
53
- return new Error("manta-codegen: the login was refused in the browser.");
54
- case "expired_token":
55
- return new Error("manta-codegen: the login code expired. Run the command again.");
56
- case "authorization_pending":
57
- case "slow_down":
58
- return undefined;
59
- default:
60
- return new Error(`manta-codegen: the device login failed with "${error ?? status}".`);
61
- }
62
- }
63
- function deviceTokenRequest(client, deviceCode, codeVerifier) {
64
- return formRequest({
65
- grant_type: deviceCodeGrant,
66
- device_code: deviceCode,
67
- client_id: client.clientId,
68
- code_verifier: codeVerifier,
69
- });
70
- }
71
- export async function pollForDeviceToken(...args) {
72
- return (await pollForDeviceTokens(...args)).accessToken;
73
- }
74
- export async function pollForDeviceTokens(client, { deviceCode, codeVerifier, intervalMs = defaultIntervalMs, expiresInMs = defaultExpiresInMs, }, { fetchImpl = fetch, sleep = sleepFor, clock = Date.now, } = {}) {
75
- const endpoint = tokenEndpoint(client.baseUrl, client.realm);
76
- const request = deviceTokenRequest(client, deviceCode, codeVerifier);
77
- const deadline = clock() + expiresInMs;
78
- let wait = intervalMs;
79
- for (;;) {
80
- // Keycloak answers authorization_pending for ever if nobody opens the link.
81
- if (clock() >= deadline)
82
- throw new Error("manta-codegen: nobody approved the login in time. Run it again.");
83
- const response = await postTo(endpoint, request, fetchImpl);
84
- const body = await readJson(response, endpoint);
85
- if (response.ok && body.access_token) {
86
- return { accessToken: body.access_token, ...(body.refresh_token ? { refreshToken: body.refresh_token } : {}) };
87
- }
88
- const failure = deviceLoginFailure(body.error, response.status);
89
- if (failure)
90
- throw failure;
91
- if (body.error === "slow_down")
92
- wait += slowDownStepMs;
93
- await sleep(wait);
94
- }
95
- }
96
- // A refresh token outlives many access tokens, so a long run renews without a second browser login.
97
- export async function refreshAccessToken(client, refreshToken, fetchImpl = fetch) {
98
- const endpoint = tokenEndpoint(client.baseUrl, client.realm);
99
- const request = formRequest({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: client.clientId });
100
- const response = await postTo(endpoint, request, fetchImpl);
101
- // A refused refresh token is terminal and earns a new login; an overloaded Keycloak must not cost one.
102
- if (isTransientStatus(response.status)) {
103
- throw new Error(`manta-codegen: ${endpoint} could not renew the token (status ${response.status}).`);
104
- }
105
- if (!response.ok)
106
- return undefined;
107
- const body = await readJson(response, endpoint);
108
- if (!body.access_token)
109
- return undefined;
110
- return { accessToken: body.access_token, refreshToken: body.refresh_token ?? refreshToken };
111
- }
112
- // One device login, then silent refreshes until the refresh token itself is refused.
113
- export function deviceLoginTokenSource(client, prompt, { fetchImpl = fetch, login = startDeviceLogin, poll = pollForDeviceTokens, } = {}) {
114
- let refreshToken;
115
- return async () => {
116
- const refreshed = refreshToken ? await refreshAccessToken(client, refreshToken, fetchImpl) : undefined;
117
- if (refreshed) {
118
- refreshToken = refreshed.refreshToken;
119
- return refreshed.accessToken;
120
- }
121
- const started = await login(client, fetchImpl);
122
- prompt(started);
123
- const tokens = await poll(client, started, { fetchImpl });
124
- refreshToken = tokens.refreshToken;
125
- return tokens.accessToken;
126
- };
127
- }
package/dist/http.d.ts DELETED
@@ -1,7 +0,0 @@
1
- export type FetchLike = (url: string, init: RequestInit) => Promise<Response>;
2
- export declare function formRequest(fields: Record<string, string>): RequestInit;
3
- export declare function isTransientStatus(status: number): boolean;
4
- export declare function readJson<T>(response: Response, endpoint: string, label?: string): Promise<T>;
5
- export declare function postTo(endpoint: string, init: RequestInit, fetchImpl?: FetchLike): Promise<Response>;
6
- export declare function describeError(error: unknown): string;
7
- //# sourceMappingURL=http.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE9E,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,WAAW,CAMvE;AAGD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzD;AAGD,wBAAsB,QAAQ,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,SAAkB,GAAG,OAAO,CAAC,CAAC,CAAC,CAQ3G;AAGD,wBAAsB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,GAAE,SAAiB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAMjH;AAGD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAIpD"}
package/dist/http.js DELETED
@@ -1,36 +0,0 @@
1
- export function formRequest(fields) {
2
- return {
3
- method: "POST",
4
- headers: { "content-type": "application/x-www-form-urlencoded" },
5
- body: new URLSearchParams(fields).toString(),
6
- };
7
- }
8
- // Overload and timeout, which are worth repeating; every other refusal is the server's final word.
9
- export function isTransientStatus(status) {
10
- return status >= 500 || status === 429 || status === 408;
11
- }
12
- // A proxy answers an HTML page, and response.json() then throws a bare SyntaxError.
13
- export async function readJson(response, endpoint, label = "manta-codegen") {
14
- const body = await response.text();
15
- try {
16
- return JSON.parse(body);
17
- }
18
- catch {
19
- throw new Error(`${label}: ${endpoint} answered status ${response.status} with text, not JSON: ${body}`);
20
- }
21
- }
22
- // A thrown fetch says only "fetch failed", which names neither the host nor the step.
23
- export async function postTo(endpoint, init, fetchImpl = fetch) {
24
- try {
25
- return await fetchImpl(endpoint, init);
26
- }
27
- catch (cause) {
28
- throw new Error(`manta-codegen: cannot reach ${endpoint}.`, { cause });
29
- }
30
- }
31
- // postTo puts the real failure in the cause, and the message alone names only the host.
32
- export function describeError(error) {
33
- if (!(error instanceof Error))
34
- return String(error);
35
- return error.cause instanceof Error ? `${error.message}\n caused by: ${error.cause.message}` : error.message;
36
- }
package/dist/token.d.ts DELETED
@@ -1,17 +0,0 @@
1
- import { type FetchLike } from "./http.js";
2
- export type ClientCredentials = {
3
- baseUrl: string;
4
- realm: string;
5
- clientId: string;
6
- clientSecret: string;
7
- scope?: string;
8
- };
9
- export type TokenSource = () => Promise<string>;
10
- export declare const tokenExpiryBufferMs = 60000;
11
- export declare const unparsableTokenTtlMs = 30000;
12
- export declare function tokenEndpoint(baseUrl: string, realm: string): string;
13
- export declare function accessTokenExpiresAt(accessToken: string, now: number): number;
14
- export declare function isFresh(expiresAt: number, now: number): boolean;
15
- export declare function mintWithClientCredentials(credentials: ClientCredentials, fetchImpl?: FetchLike): Promise<string>;
16
- export declare function cachedToken(mint: TokenSource, clock?: () => number): TokenSource;
17
- //# sourceMappingURL=token.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAuB,MAAM,WAAW,CAAC;AAEhE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;AAGhD,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAG1C,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAE3C,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpE;AAGD,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAc7E;AAED,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAE/D;AAqBD,wBAAsB,yBAAyB,CAC7C,WAAW,EAAE,iBAAiB,EAC9B,SAAS,GAAE,SAAiB,GAC3B,OAAO,CAAC,MAAM,CAAC,CAajB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,GAAE,MAAM,MAAiB,GAAG,WAAW,CAc1F"}
package/dist/token.js DELETED
@@ -1,63 +0,0 @@
1
- import { formRequest, postTo } from "./http.js";
2
- // Re-mint before Keycloak's own exp, so an in-flight request never races an expired token.
3
- export const tokenExpiryBufferMs = 60_000;
4
- // The exp claim did not parse, so the token gets a short life rather than none.
5
- export const unparsableTokenTtlMs = 30_000;
6
- export function tokenEndpoint(baseUrl, realm) {
7
- return `${baseUrl}/realms/${realm}/protocol/openid-connect/token`;
8
- }
9
- // Reads exp without checking the signature: the caller only decides when to re-mint.
10
- export function accessTokenExpiresAt(accessToken, now) {
11
- const fallback = now + unparsableTokenTtlMs;
12
- const payload = accessToken.split(".")[1];
13
- if (!payload)
14
- return fallback;
15
- try {
16
- const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
17
- const exp = claims.exp;
18
- return typeof exp === "number" ? exp * 1000 - tokenExpiryBufferMs : fallback;
19
- }
20
- catch {
21
- return fallback;
22
- }
23
- }
24
- export function isFresh(expiresAt, now) {
25
- return expiresAt > now;
26
- }
27
- function clientCredentialsRequest({ clientId, clientSecret, scope }) {
28
- return formRequest({
29
- grant_type: "client_credentials",
30
- client_id: clientId,
31
- client_secret: clientSecret,
32
- ...(scope ? { scope } : {}),
33
- });
34
- }
35
- function readAccessToken(body, realm) {
36
- const accessToken = body.access_token;
37
- if (typeof accessToken !== "string") {
38
- throw new Error(`manta-codegen: the Keycloak response for realm ${realm} carries no access_token.`);
39
- }
40
- return accessToken;
41
- }
42
- export async function mintWithClientCredentials(credentials, fetchImpl = fetch) {
43
- const { baseUrl, realm } = credentials;
44
- const endpoint = tokenEndpoint(baseUrl, realm);
45
- const response = await postTo(endpoint, clientCredentialsRequest(credentials), fetchImpl);
46
- if (!response.ok) {
47
- throw new Error(`manta-codegen: Keycloak refused the client credentials for realm ${realm} with status ${response.status}. ` +
48
- `Check the client id and the secret against ${endpoint}.`);
49
- }
50
- return readAccessToken(await response.json(), realm);
51
- }
52
- export function cachedToken(mint, clock = Date.now) {
53
- let held;
54
- return async () => {
55
- const now = clock();
56
- if (held && isFresh(held.expiresAt, now))
57
- return held.token;
58
- const token = await mint();
59
- // A device login blocks until the browser answers, so `now` is already stale here.
60
- held = { token, expiresAt: accessTokenExpiresAt(token, clock()) };
61
- return token;
62
- };
63
- }