@manta-eu/upload 0.1.2 → 0.1.3

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
@@ -14,24 +14,25 @@ pnpm manta-upload people.csv orders.parquet \
14
14
  --api-url https://app.example \
15
15
  --datastore your-datastore \
16
16
  --keycloak-url https://keycloak.example \
17
- --keycloak-realm your-realm \
18
- --client-id your-realm-app
17
+ --keycloak-realm your-realm
19
18
  ```
20
19
 
21
20
  For each file the CLI prints the file's IRI, its media type and its size.
22
21
 
23
22
  ## Log in
24
23
 
25
- Login works as it does for `manta-codegen`, whose token code this package reuses.
26
- `MANTA_CLIENT_SECRET` decides the path.
24
+ Login works as it does for `manta-codegen`: both CLIs register the same commands from
25
+ `@manta-eu/auth`. `MANTA_CLIENT_SECRET` decides the path.
27
26
 
28
- **No secret: a person.** The CLI starts the OAuth device flow with PKCE, prints a link and a code,
29
- then waits for the browser. The upload is attributed to that person. The CLI keeps the refresh token,
30
- so a long batch renews its access token silently; it asks for a new browser login only if Keycloak
31
- refuses the refresh token, for example once the session's own lifetime ends.
27
+ **No secret: a person.** Log in once with `manta-upload login`, which takes the same
28
+ `--keycloak-url` and `--keycloak-realm`; `status` and `logout` sit next to it. The
29
+ upload is attributed to that person. The login is stored and renewed silently, and it is the same
30
+ stored login `manta-codegen` uses, so logging in with either CLI serves both. See
31
+ [`@manta-eu/auth`](https://www.npmjs.com/package/@manta-eu/auth) for how the browser login
32
+ works, where the login is stored and how long it lasts.
32
33
 
33
- **A secret: a machine.** The CLI asks for a service account token. Add `--scope` for a narrower
34
- token.
34
+ **A secret: a machine.** The CLI asks for a service account token, from the realm's `manta-codegen`
35
+ client unless `--client-id` names another. Add `--scope` for a narrower token.
35
36
 
36
37
  ```shell
37
38
  export MANTA_CLIENT_SECRET=... # never a flag: an argument list is public on the machine
package/dist/cli.js CHANGED
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import { cachedToken, describeError, deviceLoginTokenSource, mintWithClientCredentials, } from "@manta-eu/codegen";
2
+ import { cachedToken, ciClientId, clientIdHelp, describeError, mintWithClientCredentials, publicClientFrom, registerAuthCommands, userTokenSource, } from "@manta-eu/auth";
3
3
  import { Command } from "commander";
4
4
  import { graphqlClient, graphqlEndpoint } from "./graphql.js";
5
5
  import { uploadFile } from "./upload.js";
6
6
  // The secret stays out of the flags: an argument list is readable to every process on the machine.
7
7
  function tokenSourceFor(options) {
8
- const client = { baseUrl: options.keycloakUrl, realm: options.keycloakRealm, clientId: options.clientId };
9
8
  const secret = process.env.MANTA_CLIENT_SECRET;
10
9
  if (secret) {
10
+ const client = publicClientFrom({ ...options, clientId: options.clientId ?? ciClientId });
11
11
  return cachedToken(() => mintWithClientCredentials({ ...client, clientSecret: secret, scope: options.scope }));
12
12
  }
13
- // Refreshes silently, so a long batch does not stop for a second browser login.
14
- return cachedToken(deviceLoginTokenSource(client, (login) => console.error(`manta-upload: open ${login.verificationUri} and enter the code ${login.userCode}`)));
13
+ // The same stored login manta-codegen uses, renewed silently, so a long batch never stops for a browser.
14
+ return userTokenSource(publicClientFrom(options), { label: "manta-upload", useDeviceCode: options.useDeviceCode });
15
15
  }
16
16
  async function upload(paths, options) {
17
17
  const graphql = graphqlClient({
@@ -38,21 +38,27 @@ async function upload(paths, options) {
38
38
  }
39
39
  const program = new Command()
40
40
  .name("manta-upload")
41
- .description("Upload files to a Manta datastore. CSV, Parquet, Turtle and N-Triples files are ingested into its knowledge graph.")
41
+ .description("Upload files to a Manta datastore. CSV, Parquet, Turtle and N-Triples files are ingested into its knowledge graph.");
42
+ // The default command, so `manta-upload <files...>` keeps working next to login, logout and status.
43
+ program
44
+ .command("upload", { isDefault: true })
45
+ .description("upload the files; the default when no other command is named")
42
46
  .argument("<files...>", "files to upload")
43
47
  .requiredOption("--api-url <url>", "Manta API base URL")
44
48
  .requiredOption("--datastore <id>", "datastore to upload into")
45
49
  .requiredOption("--keycloak-url <url>", "Keycloak base URL")
46
50
  .requiredOption("--keycloak-realm <realm>", "Keycloak realm")
47
- .requiredOption("--client-id <id>", "Keycloak client")
51
+ .option("--client-id <id>", clientIdHelp)
48
52
  .option("--scope <scope>", "scope to ask a service account token for")
53
+ .option("--use-device-code", "if a login is needed, use a one-time code instead of a browser redirect")
49
54
  .action(async (paths, options) => {
50
55
  try {
51
56
  await upload(paths, options);
52
57
  }
53
58
  catch (error) {
54
- console.error(describeError(error));
59
+ console.error(`manta-upload: ${describeError(error)}`);
55
60
  process.exitCode = 1;
56
61
  }
57
62
  });
63
+ registerAuthCommands(program, { label: "manta-upload" });
58
64
  await program.parseAsync();
package/dist/graphql.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { type FetchLike } from "@manta-eu/codegen";
1
+ import { type FetchLike, type TokenSource } from "@manta-eu/auth";
2
2
  export declare function graphqlEndpoint(apiUrl: string): string;
3
3
  export type GraphqlClient = <T>(query: string, variables: Record<string, unknown>) => Promise<T>;
4
4
  export declare function graphqlClient(options: {
5
5
  endpoint: string;
6
6
  datastore: string;
7
- token: () => Promise<string>;
7
+ token: TokenSource;
8
8
  fetchImpl?: FetchLike;
9
9
  }): GraphqlClient;
10
10
  //# sourceMappingURL=graphql.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../src/graphql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAY,MAAM,mBAAmB,CAAC;AAE5E,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;AAGjG,wBAAgB,aAAa,CAAC,OAAO,EAAE;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7B,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB,GAAG,aAAa,CAqChB"}
1
+ {"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../src/graphql.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EAGd,KAAK,WAAW,EAEjB,MAAM,gBAAgB,CAAC;AAExB,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;AAGjG,wBAAgB,aAAa,CAAC,OAAO,EAAE;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,WAAW,CAAC;IACnB,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB,GAAG,aAAa,CAmChB"}
package/dist/graphql.js CHANGED
@@ -1,13 +1,13 @@
1
- import { describeError, readJson } from "@manta-eu/codegen";
1
+ import { describeError, readJson, RefusedRequestError, withRenewal, } from "@manta-eu/auth";
2
2
  export function graphqlEndpoint(apiUrl) {
3
3
  return `${apiUrl.replace(/\/$/, "")}/graphql`;
4
4
  }
5
5
  // The datastore travels as a header: the API refuses the call rather than fall back to another one.
6
6
  export function graphqlClient(options) {
7
7
  const { endpoint, datastore, token, fetchImpl = fetch } = options;
8
- return async (query, variables) => {
9
- // Minted outside the try: a login that fails is not the API being unreachable.
10
- const authorization = `Bearer ${await token()}`;
8
+ return (query, variables) => withRenewal(token, (accessToken) => send(accessToken, query, variables));
9
+ async function send(accessToken, query, variables) {
10
+ const authorization = `Bearer ${accessToken}`;
11
11
  let response;
12
12
  try {
13
13
  response = await fetchImpl(endpoint, {
@@ -17,17 +17,17 @@ export function graphqlClient(options) {
17
17
  });
18
18
  }
19
19
  catch (cause) {
20
- throw new Error(`manta-upload: cannot reach ${endpoint}. ${describeError(cause)}`, { cause });
20
+ throw new Error(`cannot reach ${endpoint}. ${describeError(cause)}`, { cause });
21
21
  }
22
- const body = await readJson(response, endpoint, "manta-upload");
22
+ const body = await readJson(response, endpoint);
23
23
  if (body.errors?.length) {
24
- throw new Error(`manta-upload: ${body.errors.map((error) => error.message).join("; ")}`);
24
+ throw new Error(`${body.errors.map((error) => error.message).join("; ")}`);
25
25
  }
26
26
  if (!response.ok || !body.data) {
27
27
  // The Worker's auth middleware answers before Yoga, so its reason is `error`, not `errors`.
28
28
  const reason = typeof body.error === "string" ? `: ${body.error}` : " without data";
29
- throw new Error(`manta-upload: ${endpoint} answered status ${response.status}${reason}`);
29
+ throw new RefusedRequestError(`${endpoint} answered status ${response.status}${reason}`, response.status);
30
30
  }
31
31
  return body.data;
32
- };
32
+ }
33
33
  }
package/dist/upload.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type FetchLike } from "@manta-eu/codegen";
1
+ import { type FetchLike } from "@manta-eu/auth";
2
2
  import type { GraphqlClient } from "./graphql.js";
3
3
  export declare const uploadPartSize: number;
4
4
  export type UploadedFile = {
@@ -1 +1 @@
1
- {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,SAAS,EAAqB,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGlD,eAAO,MAAM,cAAc,QAAkB,CAAC;AAoB9C,MAAM,MAAM,YAAY,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAExG,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC5G,CAAC;AAGF,wBAAsB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAapG;AAED,MAAM,MAAM,KAAK,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAiDlD,wBAAsB,UAAU,CAAC,OAAO,EAAE;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,aAAa,CAAC;IACvB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,KAAK,CAAC,EAAE,KAAK,CAAC;CACf,GAAG,OAAO,CAAC,YAAY,CAAC,CASxB"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,SAAS,EAAqB,MAAM,gBAAgB,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGlD,eAAO,MAAM,cAAc,QAAkB,CAAC;AAoB9C,MAAM,MAAM,YAAY,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAExG,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC5G,CAAC;AAGF,wBAAsB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAapG;AAED,MAAM,MAAM,KAAK,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAiDlD,wBAAsB,UAAU,CAAC,OAAO,EAAE;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,aAAa,CAAC;IACvB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,KAAK,CAAC,EAAE,KAAK,CAAC;CACf,GAAG,OAAO,CAAC,YAAY,CAAC,CASxB"}
package/dist/upload.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { open } from "node:fs/promises";
3
3
  import { basename } from "node:path";
4
- import { isTransientStatus } from "@manta-eu/codegen";
4
+ import { isTransientStatus } from "@manta-eu/auth";
5
5
  // Must equal agents' uploadPartSize: the API presigns one part per this many bytes.
6
6
  export const uploadPartSize = 8 * 1024 * 1024;
7
7
  const initiateMutation = `mutation Initiate($input: InitiateUploadInput!) {
@@ -51,14 +51,14 @@ async function putPartOnce(url, body, fetchImpl) {
51
51
  if (!response.ok) {
52
52
  // The storage error document is the only thing that says why a part was refused.
53
53
  const detail = (await response.text().catch(() => "")).slice(0, 500);
54
- const message = `manta-upload: storage refused a part with status ${response.status}: ${detail}`;
54
+ const message = `storage refused a part with status ${response.status}: ${detail}`;
55
55
  throw isTransientStatus(response.status) ? new Error(message) : new NonRetryablePartError(message);
56
56
  }
57
57
  // The part is already stored, so a body that cannot be discarded must not cost a re-PUT.
58
58
  await response.body?.cancel().catch(() => undefined);
59
59
  // The part is stored, so repeating the PUT cannot conjure the header completeUpload needs.
60
60
  if (!etag) {
61
- throw new NonRetryablePartError("manta-upload: storage stored a part but answered without an ETag");
61
+ throw new NonRetryablePartError("storage stored a part but answered without an ETag");
62
62
  }
63
63
  return etag;
64
64
  }
@@ -77,12 +77,12 @@ async function uploadHandle(file, options) {
77
77
  const { path, fileName, graphql, fetchImpl = fetch, sleep = sleepFor } = options;
78
78
  const byteSize = (await file.stat()).size;
79
79
  if (byteSize === 0)
80
- throw new Error(`manta-upload: ${path} is empty`);
80
+ throw new Error(`${path} is empty`);
81
81
  const { initiateUpload } = await graphql(initiateMutation, { input: { fileName, byteSize } });
82
82
  const { uploadId, storageKey, partUrls } = initiateUpload;
83
83
  try {
84
84
  if (partUrls.length !== Math.ceil(byteSize / uploadPartSize)) {
85
- throw new Error(`manta-upload: the API split ${fileName} into ${partUrls.length} parts, which this client cannot fill`);
85
+ throw new Error(`the API split ${fileName} into ${partUrls.length} parts, which this client cannot fill`);
86
86
  }
87
87
  const parts = [];
88
88
  // Hashed from the bytes that went up, so the hash describes the stored object.
@@ -91,7 +91,7 @@ async function uploadHandle(file, options) {
91
91
  const start = (partNumber - 1) * uploadPartSize;
92
92
  const body = await readFully(file, start, uploadPartSize);
93
93
  if (body.length !== Math.min(uploadPartSize, byteSize - start)) {
94
- throw new Error(`manta-upload: ${fileName} changed while it was being read`);
94
+ throw new Error(`${fileName} changed while it was being read`);
95
95
  }
96
96
  hash.update(body);
97
97
  parts.push({ partNumber, etag: await putPart(url, body, fetchImpl, sleep) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manta-eu/upload",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "CLI that uploads files to a Manta datastore, where supported types are ingested into its knowledge graph",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "license": "MIT",
17
17
  "dependencies": {
18
18
  "commander": "^15.0.0",
19
- "@manta-eu/codegen": "0.1.2"
19
+ "@manta-eu/auth": "0.1.3"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^24.13.3",
@@ -24,8 +24,8 @@
24
24
  "@typescript-eslint/parser": "^8.56.1",
25
25
  "eslint": "^9.39.2",
26
26
  "rimraf": "^6.1.3",
27
- "typescript": "^5.0.0",
28
- "vitest": "^3.0.0"
27
+ "typescript": "^5.9.3",
28
+ "vitest": "^4.1.8"
29
29
  },
30
30
  "main": "./dist/index.js",
31
31
  "types": "./dist/index.d.ts",