i-repo 2.10.1 → 2.12.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.
@@ -9,6 +9,7 @@ import { IReporterClient } from "../sdk/index.js";
9
9
  import { getConfig, getConfigWithSource, clearStoredCredentials, } from "../config/store.js";
10
10
  import { getUserConfigPath, getProjectConfigPath, getLocalConfigPath, findProjectRoot, } from "../config/scoped-store.js";
11
11
  import { assertEndpointAllowed, persistCredentials } from "../config/client.js";
12
+ import { fingerprintEndpoint } from "../config/endpoint-fingerprint.js";
12
13
  import { logHttp } from "../core/logger.js";
13
14
  import { printSuccess, printError, printWarning, printInfo, printDetails, printInteractionHint, withSpinner, t, } from "../ui/index.js";
14
15
  import { t as tr } from "../i18n/index.js";
@@ -202,6 +203,11 @@ authCommand
202
203
  ? `Available ${t.dim(`[${passwordSrc}]`)}`
203
204
  : t.muted("Not set"),
204
205
  };
206
+ // 現テナントの権威ある指紋。DC が config を読み直して自前指紋化する経路を
207
+ // CLI 自己申告に置換できる導線 (SCHEMA.md §2.5a、reports の provenance と同一規則)。
208
+ if (endpoint) {
209
+ display["Fingerprint"] = fingerprintEndpoint(endpoint);
210
+ }
205
211
  // Config file paths
206
212
  display["User config"] = getUserConfigPath();
207
213
  const projPath = getProjectConfigPath();
@@ -1,5 +1,6 @@
1
1
  import { formatOutput } from "../../ui/formatters/index.js";
2
2
  import { getContract } from "../../contracts/registry.js";
3
+ import { buildProvenance } from "../../config/provenance.js";
3
4
  import { assertNumericId, handleError } from "../../core/errors.js";
4
5
  import { t as tr } from "../../i18n/index.js";
5
6
  export function registerReportsGet(parent, getClient) {
@@ -29,6 +30,8 @@ export function registerReportsGet(parent, getClient) {
29
30
  quietField: contract.quietField,
30
31
  render: contract.render,
31
32
  envelope: contract.envelope ?? undefined,
33
+ // 実際に叩いた接続先を機械可読で自己申告 (SCHEMA.md §2.5a)
34
+ provenance: contract.includeProvenance ? buildProvenance(globalOpts) : undefined,
32
35
  });
33
36
  }
34
37
  catch (error) {
@@ -1,6 +1,7 @@
1
1
  import { PublicStatus, EditStatus } from "../../sdk/index.js";
2
2
  import { formatOutput } from "../../ui/formatters/index.js";
3
3
  import { getContract } from "../../contracts/registry.js";
4
+ import { buildProvenance } from "../../config/provenance.js";
4
5
  import { handleError } from "../../core/errors.js";
5
6
  import { choiceOption, collectSystemKeys, parseChoiceList, parseDateTime, parseLabelId, parseNumericList, } from "../shared-options.js";
6
7
  const editStatusMap = {
@@ -52,6 +53,8 @@ export function registerReportsList(parent, getClient) {
52
53
  quietField: contract.quietField,
53
54
  render: contract.render,
54
55
  envelope: contract.envelope ?? undefined,
56
+ // 実際に叩いた接続先を機械可読で自己申告 (SCHEMA.md §2.5a)
57
+ provenance: contract.includeProvenance ? buildProvenance(globalOpts) : undefined,
55
58
  });
56
59
  }
57
60
  catch (error) {
@@ -27,6 +27,8 @@ export interface SchemaRow {
27
27
  quietField?: string;
28
28
  /** 表示ラベル変換(table/json/csv のみ適用。ndjson は生コード値) */
29
29
  labels?: Record<string, Record<string, string>>;
30
+ /** ndjson 封筒に接続来歴 provenance を付与するか(SCHEMA.md §2.5a) */
31
+ provenance?: boolean;
30
32
  }
31
33
  /** query(recordType / コマンドパス / コマンド語)で契約を絞り込んで行を組む */
32
34
  export declare function buildSchemaRows(query?: string): SchemaRow[];
@@ -43,6 +43,7 @@ export function buildSchemaRows(query) {
43
43
  columns: c.columns,
44
44
  quietField: c.quietField,
45
45
  labels: c.render,
46
+ provenance: c.includeProvenance ? true : undefined,
46
47
  }));
47
48
  }
48
49
  export const schemaCommand = new Command("schema")
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { IReporterClient } from "../sdk/index.js";
16
16
  import type { ConfigScope } from "./scoped-store.js";
17
+ import type { Provenance } from "./provenance.js";
17
18
  export interface CreateClientOptions {
18
19
  endpoint?: string;
19
20
  user?: string;
@@ -76,3 +77,14 @@ export declare function persistCredentials(args: {
76
77
  * CLI flags -> environment variables -> config store (scoped) -> interactive prompt.
77
78
  */
78
79
  export declare function createClient(opts?: CreateClientOptions): Promise<IReporterClient>;
80
+ /**
81
+ * Like createClient(), but also returns the provenance of the connection it
82
+ * resolved (endpoint + fingerprint + user + resolution source). Thin wrapper:
83
+ * createClient remains the single source of truth for credential resolution and
84
+ * stays unchanged for backward compatibility. The password is never surfaced in
85
+ * provenance. See ./provenance.ts and docs/SCHEMA.md §2.5a.
86
+ */
87
+ export declare function createClientWithProvenance(opts?: CreateClientOptions): Promise<{
88
+ client: IReporterClient;
89
+ provenance: Provenance;
90
+ }>;
@@ -18,6 +18,7 @@ import { logHttp } from "../core/logger.js";
18
18
  import { ConfigError } from "../core/errors.js";
19
19
  import { getConfig, setConfig } from "./store.js";
20
20
  import { getConfigPath, getUserConfigPath } from "./scoped-store.js";
21
+ import { buildProvenance, resolveEndpointUser } from "./provenance.js";
21
22
  /** Env var that opts in to plain-http on ANY host (including public ones). */
22
23
  export const INSECURE_ENV = "IREPO_ALLOW_INSECURE";
23
24
  /** Whether the user has explicitly opted in to insecure (plain http) endpoints. */
@@ -137,8 +138,10 @@ let cachedPassword;
137
138
  * CLI flags -> environment variables -> config store (scoped) -> interactive prompt.
138
139
  */
139
140
  export async function createClient(opts = {}) {
140
- const endpoint = opts.endpoint || process.env.IREPO_ENDPOINT || getConfig("endpoint");
141
- const user = opts.user || process.env.IREPO_USER || getConfig("user");
141
+ // Endpoint/user resolution is shared with buildProvenance() so provenance can
142
+ // never drift from the connection this client actually uses. Password is
143
+ // resolved here only (never surfaced in provenance).
144
+ const { endpoint, user } = resolveEndpointUser(opts);
142
145
  let password = opts.password || process.env.IREPO_PASSWORD || cachedPassword || getConfig("password");
143
146
  if (!endpoint) {
144
147
  // 素の Error だと exit 1 (unknown) に落ちて exit code 契約
@@ -176,3 +179,14 @@ export async function createClient(opts = {}) {
176
179
  onHttp: logHttp,
177
180
  });
178
181
  }
182
+ /**
183
+ * Like createClient(), but also returns the provenance of the connection it
184
+ * resolved (endpoint + fingerprint + user + resolution source). Thin wrapper:
185
+ * createClient remains the single source of truth for credential resolution and
186
+ * stays unchanged for backward compatibility. The password is never surfaced in
187
+ * provenance. See ./provenance.ts and docs/SCHEMA.md §2.5a.
188
+ */
189
+ export async function createClientWithProvenance(opts = {}) {
190
+ const client = await createClient(opts);
191
+ return { client, provenance: buildProvenance(opts) };
192
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Normalize an endpoint URL for identity comparison (endpoint.rs::normalize).
3
+ * Empty/whitespace-only input returns "" so callers can treat it as "unresolved"
4
+ * (fail-closed) rather than fingerprinting a blank.
5
+ */
6
+ export declare function normalizeEndpoint(raw: string): string;
7
+ /**
8
+ * SHA-256 hex (64 chars) of the normalized endpoint — the "which server" id.
9
+ * Byte-identical to data-connect's `endpoint::fingerprint`. Returns "" when the
10
+ * endpoint is unresolved (normalize → ""), so an unset endpoint never collides
11
+ * with a real one.
12
+ */
13
+ export declare function fingerprintEndpoint(url: string): string;
14
+ /**
15
+ * Masked host for readable display (`scheme://host[:non-default-port]`), dropping
16
+ * userinfo, path, query and fragment. Port-stripping is NOT applied here and case
17
+ * is preserved — mirrors data-connect's `endpoint::mask_host`.
18
+ */
19
+ export declare function maskEndpointHost(url: string): string;
20
+ /**
21
+ * A human-readable, secret-free rendering of an endpoint for provenance display:
22
+ * `scheme://host[:port]/path`, preserving case verbatim but **dropping every place
23
+ * a credential can hide** — `userinfo@`, the query string and the fragment. Lets
24
+ * provenance carry the real endpoint without leaking secrets embedded in the URL
25
+ * (no-secret invariant). A scheme-less fragment keeps its host/path but still has
26
+ * query/fragment stripped.
27
+ */
28
+ export declare function safeEndpointDisplay(url: string): string;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Endpoint identity helpers — the byte-for-byte contract shared with consumers.
3
+ *
4
+ * `fingerprintEndpoint` MUST produce the same hex as data-connect's
5
+ * `endpoint::fingerprint` (src-tauri/src/endpoint.rs). Consumers persist these
6
+ * fingerprints (e.g. data-connect's connector.db endpoint_fp columns); any drift
7
+ * in the normalization rule would make existing rows fail-closed and "lose"
8
+ * already-delivered data. The rule is therefore FROZEN — this is a verbatim port
9
+ * of the current endpoint.rs::normalize, not an "improved" one. See docs/SCHEMA.md
10
+ * for the canonical published spec and src/__tests__/endpoint-fingerprint.test.ts
11
+ * for the ported cross-language test vectors that guard the two implementations.
12
+ *
13
+ * Normalization (verbatim port of endpoint.rs::normalize):
14
+ * trim → lowercase the WHOLE url (path included) → split scheme on first "://"
15
+ * → split authority/path on first "/" → drop userinfo (right of last "@") →
16
+ * strip default port (exact ":443" for https / ":80" for http; non-default
17
+ * kept) → trim trailing "/" from path (drop if empty) → reassemble.
18
+ *
19
+ * Footnote: a host that is non-ASCII (IDN) could lowercase differently between
20
+ * Rust `to_lowercase` and JS `toLowerCase`. i-Reporter endpoints are ASCII, so
21
+ * this is a documented non-issue rather than a divergence in practice.
22
+ */
23
+ import { createHash } from "node:crypto";
24
+ /**
25
+ * Normalize an endpoint URL for identity comparison (endpoint.rs::normalize).
26
+ * Empty/whitespace-only input returns "" so callers can treat it as "unresolved"
27
+ * (fail-closed) rather than fingerprinting a blank.
28
+ */
29
+ export function normalizeEndpoint(raw) {
30
+ const lowered = raw.trim().toLowerCase();
31
+ if (lowered === "")
32
+ return "";
33
+ // scheme split on the FIRST "://"
34
+ const schemeIdx = lowered.indexOf("://");
35
+ const scheme = schemeIdx >= 0 ? lowered.slice(0, schemeIdx) : undefined;
36
+ const rest = schemeIdx >= 0 ? lowered.slice(schemeIdx + 3) : lowered;
37
+ // authority / path split on the FIRST "/"
38
+ const slashIdx = rest.indexOf("/");
39
+ const authority = slashIdx >= 0 ? rest.slice(0, slashIdx) : rest;
40
+ const rawPath = slashIdx >= 0 ? rest.slice(slashIdx + 1) : undefined;
41
+ // userinfo@host:port → host:port (split on the LAST "@")
42
+ const atIdx = authority.lastIndexOf("@");
43
+ let hostPort = atIdx >= 0 ? authority.slice(atIdx + 1) : authority;
44
+ // default-port removal: exact suffix match, scheme-gated (non-default kept).
45
+ if (scheme === "https" && hostPort.endsWith(":443")) {
46
+ hostPort = hostPort.slice(0, -":443".length);
47
+ }
48
+ else if (scheme === "http" && hostPort.endsWith(":80")) {
49
+ hostPort = hostPort.slice(0, -":80".length);
50
+ }
51
+ // trim ALL trailing slashes (Rust trim_end_matches('/')); drop empty path.
52
+ let path;
53
+ if (rawPath !== undefined) {
54
+ const trimmed = rawPath.replace(/\/+$/, "");
55
+ path = trimmed === "" ? undefined : trimmed;
56
+ }
57
+ let out = "";
58
+ if (scheme !== undefined)
59
+ out += `${scheme}://`;
60
+ out += hostPort;
61
+ if (path !== undefined)
62
+ out += `/${path}`;
63
+ return out;
64
+ }
65
+ /**
66
+ * SHA-256 hex (64 chars) of the normalized endpoint — the "which server" id.
67
+ * Byte-identical to data-connect's `endpoint::fingerprint`. Returns "" when the
68
+ * endpoint is unresolved (normalize → ""), so an unset endpoint never collides
69
+ * with a real one.
70
+ */
71
+ export function fingerprintEndpoint(url) {
72
+ const normalized = normalizeEndpoint(url);
73
+ if (normalized === "")
74
+ return "";
75
+ return createHash("sha256").update(normalized, "utf8").digest("hex");
76
+ }
77
+ /**
78
+ * Masked host for readable display (`scheme://host[:non-default-port]`), dropping
79
+ * userinfo, path, query and fragment. Port-stripping is NOT applied here and case
80
+ * is preserved — mirrors data-connect's `endpoint::mask_host`.
81
+ */
82
+ export function maskEndpointHost(url) {
83
+ const trimmed = url.trim();
84
+ const schemeIdx = trimmed.indexOf("://");
85
+ const scheme = schemeIdx >= 0 ? trimmed.slice(0, schemeIdx) : undefined;
86
+ const rest = schemeIdx >= 0 ? trimmed.slice(schemeIdx + 3) : trimmed;
87
+ const sepIdx = rest.search(/[/?#]/);
88
+ const authority = sepIdx >= 0 ? rest.slice(0, sepIdx) : rest;
89
+ const atIdx = authority.lastIndexOf("@");
90
+ const hostPort = atIdx >= 0 ? authority.slice(atIdx + 1) : authority;
91
+ return scheme !== undefined ? `${scheme}://${hostPort}` : hostPort;
92
+ }
93
+ /**
94
+ * A human-readable, secret-free rendering of an endpoint for provenance display:
95
+ * `scheme://host[:port]/path`, preserving case verbatim but **dropping every place
96
+ * a credential can hide** — `userinfo@`, the query string and the fragment. Lets
97
+ * provenance carry the real endpoint without leaking secrets embedded in the URL
98
+ * (no-secret invariant). A scheme-less fragment keeps its host/path but still has
99
+ * query/fragment stripped.
100
+ */
101
+ export function safeEndpointDisplay(url) {
102
+ const trimmed = url.trim();
103
+ const schemeIdx = trimmed.indexOf("://");
104
+ if (schemeIdx < 0) {
105
+ // No authority section (no userinfo to scrub); just drop query/fragment.
106
+ return trimmed.split(/[?#]/)[0];
107
+ }
108
+ const scheme = trimmed.slice(0, schemeIdx);
109
+ const rest = trimmed.slice(schemeIdx + 3);
110
+ const sepIdx = rest.search(/[/?#]/);
111
+ const authority = sepIdx >= 0 ? rest.slice(0, sepIdx) : rest;
112
+ // Keep only the path (from the first "/" up to "?"/"#"); query/fragment dropped.
113
+ const path = sepIdx >= 0 && rest[sepIdx] === "/" ? rest.slice(sepIdx).split(/[?#]/)[0] : "";
114
+ const atIdx = authority.lastIndexOf("@");
115
+ const hostPort = atIdx >= 0 ? authority.slice(atIdx + 1) : authority;
116
+ return `${scheme}://${hostPort}${path}`;
117
+ }
@@ -0,0 +1,43 @@
1
+ /** Where a credential value was resolved from (machine-readable contract). */
2
+ export type ResolvedFrom = "flag" | "env" | "config-user" | "config-project" | "config-local";
3
+ export interface Provenance {
4
+ /**
5
+ * The endpoint actually used, userinfo-stripped (no secret). Pre-normalization
6
+ * so humans see their real casing/port/path; the fingerprint below is the
7
+ * canonical identity.
8
+ */
9
+ endpoint: string;
10
+ /** SHA-256 of the normalized endpoint — byte-identical to DC's fingerprint. */
11
+ endpointFingerprint: string;
12
+ /** The resolved user (never the password). */
13
+ user: string;
14
+ /** Where the *endpoint* was resolved from. "" when no endpoint is configured. */
15
+ resolvedFrom: ResolvedFrom | "";
16
+ }
17
+ /**
18
+ * Machine-readable resolution source for a credential key. Mirrors the
19
+ * human-facing `resolveSource()` in commands/auth.ts but emits stable contract
20
+ * tokens (`config-user`/`config-project`/`config-local`) instead of display
21
+ * labels — the two share the same getConfigWithSource() lookup.
22
+ */
23
+ export declare function resolveSourceMachine(key: "endpoint" | "user" | "password", flagValue?: string): ResolvedFrom | "";
24
+ export interface BuildProvenanceOptions {
25
+ endpoint?: string;
26
+ user?: string;
27
+ }
28
+ /**
29
+ * Resolve endpoint + user from flag → env → config. The single source of truth
30
+ * for this chain: both createClient() and buildProvenance() call it, so the
31
+ * provenance reported alongside a result can never drift from the connection the
32
+ * request was actually made on. Password is resolved separately by createClient.
33
+ */
34
+ export declare function resolveEndpointUser(opts?: BuildProvenanceOptions): {
35
+ endpoint: string | undefined;
36
+ user: string | undefined;
37
+ };
38
+ /**
39
+ * Build the provenance for the connection these options resolve to. Uses the
40
+ * shared resolveEndpointUser() chain; resolvedFrom reports the *endpoint*'s source
41
+ * (the tenant-identifying value).
42
+ */
43
+ export declare function buildProvenance(opts?: BuildProvenanceOptions): Provenance;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Per-invocation provenance — the CLI's self-report of which connection it
3
+ * actually used. Lets consumers (e.g. data-connect) trust the CLI's own
4
+ * statement of "endpoint + fingerprint + user + where it came from" instead of
5
+ * re-reading global config and re-fingerprinting it themselves (the path where
6
+ * app belief and CLI reality drift apart). See docs/SCHEMA.md §2.5a.
7
+ *
8
+ * Resolution mirrors createClient() exactly (flag → env → config), so the
9
+ * provenance reported alongside a result describes the same connection the
10
+ * request was made on. Secrets (password) are never carried here.
11
+ */
12
+ import { getConfig, getConfigWithSource } from "./store.js";
13
+ import { fingerprintEndpoint, safeEndpointDisplay } from "./endpoint-fingerprint.js";
14
+ /**
15
+ * Machine-readable resolution source for a credential key. Mirrors the
16
+ * human-facing `resolveSource()` in commands/auth.ts but emits stable contract
17
+ * tokens (`config-user`/`config-project`/`config-local`) instead of display
18
+ * labels — the two share the same getConfigWithSource() lookup.
19
+ */
20
+ export function resolveSourceMachine(key, flagValue) {
21
+ if (flagValue)
22
+ return "flag";
23
+ const envMap = {
24
+ endpoint: process.env.IREPO_ENDPOINT,
25
+ user: process.env.IREPO_USER,
26
+ password: process.env.IREPO_PASSWORD,
27
+ };
28
+ if (envMap[key])
29
+ return "env";
30
+ const info = getConfigWithSource(key);
31
+ switch (info.source) {
32
+ case "user":
33
+ return "config-user";
34
+ case "project":
35
+ return "config-project";
36
+ case "local":
37
+ return "config-local";
38
+ default:
39
+ return "";
40
+ }
41
+ }
42
+ /**
43
+ * Resolve endpoint + user from flag → env → config. The single source of truth
44
+ * for this chain: both createClient() and buildProvenance() call it, so the
45
+ * provenance reported alongside a result can never drift from the connection the
46
+ * request was actually made on. Password is resolved separately by createClient.
47
+ */
48
+ export function resolveEndpointUser(opts = {}) {
49
+ return {
50
+ endpoint: opts.endpoint || process.env.IREPO_ENDPOINT || getConfig("endpoint"),
51
+ user: opts.user || process.env.IREPO_USER || getConfig("user"),
52
+ };
53
+ }
54
+ /**
55
+ * Build the provenance for the connection these options resolve to. Uses the
56
+ * shared resolveEndpointUser() chain; resolvedFrom reports the *endpoint*'s source
57
+ * (the tenant-identifying value).
58
+ */
59
+ export function buildProvenance(opts = {}) {
60
+ const { endpoint, user } = resolveEndpointUser(opts);
61
+ return {
62
+ endpoint: endpoint ? safeEndpointDisplay(endpoint) : "",
63
+ endpointFingerprint: fingerprintEndpoint(endpoint ?? ""),
64
+ user: user ?? "",
65
+ resolvedFrom: resolveSourceMachine("endpoint", opts.endpoint),
66
+ };
67
+ }
@@ -26,6 +26,12 @@ export interface CommandContract {
26
26
  quietField?: string;
27
27
  /** 表示ラベル変換(table/json/csv のみ。ndjson には適用されない) */
28
28
  render?: RenderMap;
29
+ /**
30
+ * true のとき ndjson 封筒に接続来歴(provenance)を付与する。
31
+ * 「どの endpoint/指紋/user・解決ソースで叩いたか」を機械可読で自己申告し、
32
+ * 消費側が config を読み直して自前指紋化する経路を不要化する(SCHEMA.md §2.5a)。
33
+ */
34
+ includeProvenance?: boolean;
29
35
  }
30
36
  /**
31
37
  * 出力契約の正本。コマンド実装はここから getContract() で引く
@@ -19,6 +19,7 @@ export const contracts = [
19
19
  columns: ["type", "itemId", "name", "editStatus", "revNo", "registTime", "updateTime"],
20
20
  quietField: "itemId",
21
21
  render: { type: typeLabels, editStatus: editStatusLabels },
22
+ includeProvenance: true,
22
23
  },
23
24
  {
24
25
  command: "reports get",
@@ -28,6 +29,7 @@ export const contracts = [
28
29
  idField: "topId",
29
30
  },
30
31
  quietField: "topId",
32
+ includeProvenance: true,
31
33
  },
32
34
  ];
33
35
  /** コマンドパスで契約を引く。未登録は実装バグなので明示エラー */
@@ -14,6 +14,8 @@ import { accessSync, constants, readdirSync, statSync } from "node:fs";
14
14
  import { homedir } from "node:os";
15
15
  import { delimiter, isAbsolute, join } from "node:path";
16
16
  import { getConfig } from "../config/store.js";
17
+ import { fingerprintEndpoint } from "../config/endpoint-fingerprint.js";
18
+ import { resolveSourceMachine } from "../config/provenance.js";
17
19
  /** Prefix every plugin executable must carry. */
18
20
  export const PLUGIN_PREFIX = "i-repo-";
19
21
  const isWin = process.platform === "win32";
@@ -246,6 +248,19 @@ export function buildPluginEnv(ctx, opts = {}) {
246
248
  env.IREPO_TIMEOUT = String(timeout);
247
249
  env.IREPO_QUIET = quiet ? "1" : "";
248
250
  env.IREPO_PLUGIN_API = "1";
251
+ // Provenance of the resolved i-Reporter endpoint, computed CLI-side — the CLI
252
+ // is the single source of truth for the fingerprint; plugins must echo these,
253
+ // never fingerprint themselves (would break the frozen rule). Authoritative:
254
+ // scrub any inherited value first (a PATH binary must not be able to spoof its
255
+ // own provenance via env), then set only when the endpoint actually resolves.
256
+ // Absence ⇒ "unknown" (consumers treat it fail-closed, never as a match).
257
+ delete env.IREPO_ENDPOINT_FINGERPRINT;
258
+ delete env.IREPO_ENDPOINT_RESOLVED_FROM;
259
+ const endpointFingerprint = endpoint ? fingerprintEndpoint(String(endpoint)) : "";
260
+ if (endpointFingerprint) {
261
+ env.IREPO_ENDPOINT_FINGERPRINT = endpointFingerprint;
262
+ env.IREPO_ENDPOINT_RESOLVED_FROM = resolveSourceMachine("endpoint", ctx.endpoint);
263
+ }
249
264
  return env;
250
265
  }
251
266
  /**
@@ -8,6 +8,7 @@
8
8
  * render, which converts codes to human labels). Applied only for
9
9
  * `--format ndjson`. See ./index.ts and docs/SCHEMA.md.
10
10
  */
11
+ import type { Provenance } from "../../config/provenance.js";
11
12
  /** Attachment carried by reference only — never the bytes (docs/SCHEMA.md). */
12
13
  export interface Attachment {
13
14
  kind: string;
@@ -58,6 +59,12 @@ export interface Envelope {
58
59
  idempotencyKey?: string;
59
60
  systemKeys: Record<string, string>;
60
61
  attachments: Attachment[];
62
+ /**
63
+ * Per-invocation connection provenance (endpoint/fingerprint/user/source),
64
+ * constant across a stream. Present only when the command opts in
65
+ * (CommandContract.includeProvenance). Never carries a secret. See SCHEMA.md §2.5a.
66
+ */
67
+ provenance?: Provenance;
61
68
  /** The complete, authoritative canonical record — verbatim, nothing dropped. */
62
69
  values: Record<string, unknown>;
63
70
  /** Promoted identifier under the field name from spec.idField, and headroom. */
@@ -78,10 +85,12 @@ export interface StreamEnd {
78
85
  count: number;
79
86
  /** "complete" on normal termination. Headroom for future streaming emitters. */
80
87
  status: "complete" | "aborted";
88
+ /** Same provenance as the records, so a consumer can read it from one line. */
89
+ provenance?: Provenance;
81
90
  }
82
91
  export declare const DEFAULT_SCHEMA_VERSION = "1.0";
83
92
  /** Build the stream-end trailer closing an envelope stream of `count` records. */
84
- export declare function buildStreamEnd(count: number, spec: Pick<EnvelopeSpec, "schemaVersion">, status?: StreamEnd["status"]): StreamEnd;
93
+ export declare function buildStreamEnd(count: number, spec: Pick<EnvelopeSpec, "schemaVersion">, status?: StreamEnd["status"], provenance?: Provenance): StreamEnd;
85
94
  /**
86
95
  * Wrap one canonical record in the stable ndjson envelope.
87
96
  *
@@ -89,4 +98,4 @@ export declare function buildStreamEnd(count: number, spec: Pick<EnvelopeSpec, "
89
98
  * - `values` is a shallow clone of the whole record — verbatim, authoritative.
90
99
  * Envelope-level fields are projections of `values`; duplication is intentional.
91
100
  */
92
- export declare function buildEnvelope(record: Record<string, unknown>, spec: EnvelopeSpec): Envelope;
101
+ export declare function buildEnvelope(record: Record<string, unknown>, spec: EnvelopeSpec, provenance?: Provenance): Envelope;
@@ -11,12 +11,13 @@
11
11
  export const DEFAULT_SCHEMA_VERSION = "1.0";
12
12
  const DEFAULT_SYSTEM_KEY_COUNT = 5;
13
13
  /** Build the stream-end trailer closing an envelope stream of `count` records. */
14
- export function buildStreamEnd(count, spec, status = "complete") {
14
+ export function buildStreamEnd(count, spec, status = "complete", provenance) {
15
15
  return {
16
16
  schemaVersion: spec.schemaVersion ?? DEFAULT_SCHEMA_VERSION,
17
17
  recordType: "stream-end",
18
18
  count,
19
19
  status,
20
+ ...(provenance !== undefined ? { provenance } : {}),
20
21
  };
21
22
  }
22
23
  /**
@@ -26,7 +27,7 @@ export function buildStreamEnd(count, spec, status = "complete") {
26
27
  * - `values` is a shallow clone of the whole record — verbatim, authoritative.
27
28
  * Envelope-level fields are projections of `values`; duplication is intentional.
28
29
  */
29
- export function buildEnvelope(record, spec) {
30
+ export function buildEnvelope(record, spec, provenance) {
30
31
  const schemaVersion = spec.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
31
32
  // Resolve the identifier from one or more candidate fields (first present,
32
33
  // non-empty wins). Emit it under that field's own name. If none are present,
@@ -61,6 +62,7 @@ export function buildEnvelope(record, spec) {
61
62
  : {}),
62
63
  systemKeys: collectSystemKeys(record, spec),
63
64
  attachments: spec.attachmentsFrom?.(record) ?? [],
65
+ ...(provenance !== undefined ? { provenance } : {}),
64
66
  values: { ...record },
65
67
  };
66
68
  }
@@ -1,5 +1,6 @@
1
1
  import type { RenderMap } from "./renderers.js";
2
2
  import type { EnvelopeSpec } from "./envelope.js";
3
+ import type { Provenance } from "../../config/provenance.js";
3
4
  /** Supported output formats (single source of truth for flag/config validation). */
4
5
  export declare const OUTPUT_FORMATS: readonly ["table", "json", "csv", "ndjson"];
5
6
  export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
@@ -16,6 +17,11 @@ export interface FormatOptions {
16
17
  render?: RenderMap;
17
18
  /** When set, ndjson records are wrapped in the stable envelope (docs/SCHEMA.md). */
18
19
  envelope?: EnvelopeSpec;
20
+ /**
21
+ * Per-invocation connection provenance. When set (and an envelope is applied),
22
+ * it is added to every envelope record and the stream-end trailer. ndjson only.
23
+ */
24
+ provenance?: Provenance;
19
25
  }
20
26
  /**
21
27
  * Format and print data to stdout based on the given options.
@@ -52,12 +52,12 @@ export function formatOutput(data, options) {
52
52
  if (options.envelope) {
53
53
  const arr = Array.isArray(data) ? data : data == null ? [] : [data];
54
54
  const spec = options.envelope;
55
- const emitted = formatNdjson(arr.map((r) => buildEnvelope(r, spec)));
55
+ const emitted = formatNdjson(arr.map((r) => buildEnvelope(r, spec, options.provenance)));
56
56
  // Terminal trailer: lets downstream distinguish "complete" from a broken
57
57
  // pipe and verify count — absence means the stream was truncated (§2.6).
58
58
  // Count = lines actually written (a serialization-failed record is
59
59
  // reported on stderr and excluded, keeping the count verifiable).
60
- formatNdjson(buildStreamEnd(emitted, spec));
60
+ formatNdjson(buildStreamEnd(emitted, spec, "complete", options.provenance));
61
61
  return;
62
62
  }
63
63
  formatNdjson(data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i-repo",
3
- "version": "2.10.1",
3
+ "version": "2.12.0",
4
4
  "description": "Modern CLI for ConMas i-Reporter - Built for humans and AI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,7 +44,7 @@ async function loadBuiltins() {
44
44
  }
45
45
 
46
46
  const PLUGIN = "i-repo-archive";
47
- const VERSION = "0.5.7";
47
+ const VERSION = "0.6.0";
48
48
  const PLUGIN_API = "1";
49
49
  const defaultArchiveRoot = () => join(homedir(), ".i-repo", "archives");
50
50
 
@@ -616,9 +616,11 @@ async function createArchive(options) {
616
616
 
617
617
  // Completion marker written last: its absence marks an incomplete bundle.
618
618
  const receiptPath = join(archivePath, "receipts", "archive.json");
619
+ const provenance = endpointProvenance();
619
620
  await writeJsonFile(receiptPath, {
620
621
  jobId, count: records.length,
621
622
  producedBy: { plugin: PLUGIN, version: VERSION }, // 来歴: stdout だけでなく archive.json にも残す
623
+ ...(provenance ? { provenance } : {}), // 接続来歴も stdout receipt と同一値で残す
622
624
  ...extras,
623
625
  });
624
626
  savedFiles.push(receiptPath);
@@ -898,11 +900,29 @@ async function pullArchiveBundle(options, backend) {
898
900
 
899
901
  // ── receipts (SCHEMA.md §6) ─────────────────────────────────────────────────
900
902
 
903
+ // 接続来歴: CLI(dispatch) が precomputed して env で渡した値を echo するだけ。指紋は
904
+ // 計算しない(CLI が正本=凍結規則の第2実装を作らない。SCHEMA.md §2.5a/§6)。
905
+ // IREPO_ENDPOINT_FINGERPRINT が無い=endpoint 未解決 → provenance を省略(不明=
906
+ // fail-closed; 消費側は空/不在を「一致」と解釈しない契約)。
907
+ function endpointProvenance() {
908
+ const endpointFingerprint = process.env.IREPO_ENDPOINT_FINGERPRINT;
909
+ if (!endpointFingerprint) return undefined;
910
+ return {
911
+ endpoint: process.env.IREPO_ENDPOINT || "",
912
+ endpointFingerprint,
913
+ user: process.env.IREPO_USER || "",
914
+ resolvedFrom: process.env.IREPO_ENDPOINT_RESOLVED_FROM || "",
915
+ };
916
+ }
917
+
901
918
  function emitReceipt(phase, jobId, count, failedCount, verified, extras) {
919
+ const provenance = endpointProvenance();
902
920
  process.stdout.write(JSON.stringify({
903
921
  schemaVersion: "1.0", recordType: "receipt", plugin: PLUGIN,
904
922
  producedBy: { plugin: PLUGIN, version: VERSION },
905
- phase, jobId, count, failedCount, verified, ...extras,
923
+ phase, jobId, count, failedCount, verified,
924
+ ...(provenance ? { provenance } : {}),
925
+ ...extras,
906
926
  }) + "\n");
907
927
  }
908
928