ardent-cli-beta 0.1.0 → 0.1.2

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
@@ -72,17 +72,21 @@ only the libpq form opens psql. Otherwise the credential stays out of arguments
72
72
  variables ... and redacts the username, API key and password from both `script` output
73
73
  streams". It does not stop a user from printing a credential
74
74
  deliberately through SQL or a shell escape. A mode-0600 recovery
75
- file holds only request and issuance IDs, and any source role name, until revocation succeeds, and the next `branch connect`
75
+ file holds only request and issuance IDs, and the source role name or access profile its request named, until revocation succeeds, and the next `branch connect`
76
76
  attempts to revoke every abandoned issuance before requesting another. A forced process kill
77
77
  can leave a credential active until that recovery runs. Failed cleanup retains
78
78
  the file and names the issuance ID for manual revocation.
79
79
 
80
- [openapi.json](openapi.json) ships in the npm package. It is a documentation snapshot of the control-plane
81
- specification as of 2026-09-24, from Ardent's private source revision
82
- `c8510ab3e2422756a93e1df09acf994cf6bac34d`, and not proof of deployed
83
- compatibility. It predates the branch credential endpoints used by `branch connect`, and the
84
- branch wake, suspend and recover operations used by those commands, and connector
85
- deletion; the source specification documents wake, suspend and recover, and
80
+ [openapi.json](openapi.json) ships in the npm package. Its base is the
81
+ 2026-09-24 control-plane documentation snapshot from Ardent's private source
82
+ revision `c8510ab3e2422756a93e1df09acf994cf6bac34d`. The branch-creation
83
+ description, accepted source-position fields, and seed-origin description were
84
+ selectively updated from the reviewed control-plane specification at revision
85
+ `32da7390a` for ARD-2378. The rest was not refreshed, and this file is not
86
+ proof of deployed compatibility. It still lacks the branch credential endpoints
87
+ used by `branch connect`, the branch wake, suspend and recover operations used by
88
+ those commands, and connector deletion; the source specification documents
89
+ wake, suspend and recover, and
86
90
  refreshing this snapshot from a merged revision that carries them is owed.
87
91
  Documenting the credential and connector-deletion endpoints in the source
88
92
  specification is also owed. To refresh it, replace it with the reviewed
@@ -136,8 +140,10 @@ report source cleanup residue; the delete command warns about it and status
136
140
  preserves its code in text and JSON. A failed or interrupted wait retains local
137
141
  context and names the status command before retrying.
138
142
 
139
- Creation uses the connector's latest published snapshot. If that snapshot is
140
- unavailable, creation fails without choosing an older one. Names are unique within
143
+ Creation asks the API for a branch at its recent applied source position. The
144
+ API uses a published mark covering that position or requests one on demand; an
145
+ accepted operation waits for that mark before materialization. Without recent
146
+ source progress, the API refuses creation. Names are unique within
141
147
  a connector while the branch is not deleted. `branch delete` sends the connector
142
148
  and name to the API, which resolves the target. When no branch has that name and
143
149
  the argument is the ID of a deleted branch whose erasure is unconfirmed, as a
@@ -1,3 +1,4 @@
1
+ import { type Unadmitted } from './credentials.js';
1
2
  import { type Driver } from './drivers.js';
2
3
  export interface BranchConnection {
3
4
  configDir: string;
@@ -17,5 +18,6 @@ export interface PrintedConnection {
17
18
  issuance_id: string;
18
19
  driver: Driver;
19
20
  connection_string: string;
21
+ unadmitted?: Unadmitted;
20
22
  }
21
23
  export declare function connectToBranch(connection: BranchConnection): Promise<PrintedConnection | void>;
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { acknowledgeBranchCredential, composeCredentialDelivery, CredentialIssueRefused, issuedConnectKey, issueBranchCredential, revokeBranchCredential, waitForBranchCredential } from './credentials.js';
2
+ import { acknowledgeBranchCredential, ALL_SCHEMAS_PROFILE, composeCredentialDelivery, CredentialIssueRefused, describeUnadmitted, issuedConnectKey, issueBranchCredentialWhenMarked, revokeBranchCredential, waitForBranchCredential } from './credentials.js';
3
3
  import { CLIENT_REQUIREMENTS, connectionString } from './drivers.js';
4
4
  import { connectionEnvironment, runPsql } from './psql.js';
5
5
  import { CredentialRecoveryCleanupError, forgetCredentialRecovery, openCredentialWitness, rememberCredentialIssuance, rememberCredentialRequest } from './recovery.js';
@@ -11,9 +11,12 @@ import { terminalText } from '../terminal.js';
11
11
  export async function connectToBranch(connection) {
12
12
  const { token, branchID, sourceRole, print, driver, say, warn } = connection;
13
13
  const requestID = randomUUID();
14
+ // Without a source role, branch connect asks for every in-scope schema, and
15
+ // never retries with another profile: a refusal tells the user to choose.
16
+ const credential = sourceRole === undefined ? { accessProfile: ALL_SCHEMAS_PROFILE } : { sourceRole };
14
17
  const witness = await openCredentialWitness(requestID);
15
18
  try {
16
- const recovery = await rememberCredentialRequest(connection.configDir, branchID, token, witness.port, requestID, sourceRole);
19
+ const recovery = await rememberCredentialRequest(connection.configDir, branchID, token, witness.port, requestID, credential);
17
20
  let issuanceID;
18
21
  let connectKey;
19
22
  let issueRefused = false;
@@ -26,7 +29,7 @@ export async function connectToBranch(connection) {
26
29
  let failure;
27
30
  try {
28
31
  try {
29
- const issued = await issueBranchCredential(token, branchID, requestID, sourceRole);
32
+ const issued = await issueBranchCredentialWhenMarked(token, branchID, requestID, credential, say);
30
33
  issuanceID = issued.issuanceID;
31
34
  await rememberCredentialIssuance(recovery, issuanceID);
32
35
  connectKey = issuedConnectKey(issued);
@@ -47,6 +50,9 @@ export async function connectToBranch(connection) {
47
50
  throw error;
48
51
  }
49
52
  const delivery = composeCredentialDelivery(await deliverWhenServing(token, issuanceID, connection.serving), connectKey);
53
+ const unadmitted = describeUnadmitted(delivery.unadmitted);
54
+ if (unadmitted)
55
+ say(unadmitted);
50
56
  if (print) {
51
57
  // Checked before acknowledgement, so a form that would not verify
52
58
  // the server is revoked, never handed over.
@@ -74,7 +80,7 @@ export async function connectToBranch(connection) {
74
80
  say(`The ${driver} form needs, in your client:\n${CLIENT_REQUIREMENTS[driver].map((requirement) => ` - ${requirement}`).join('\n')}`);
75
81
  }
76
82
  if (connection.json)
77
- return { issuance_id: issuanceID, driver, connection_string: value };
83
+ return { issuance_id: issuanceID, driver, connection_string: value, ...(delivery.unadmitted ? { unadmitted: delivery.unadmitted } : {}) };
78
84
  connection.log(value);
79
85
  return;
80
86
  }
@@ -4,10 +4,25 @@ export interface IssuedCredential {
4
4
  connectKey?: unknown;
5
5
  connectKeyRequired?: unknown;
6
6
  }
7
- interface DeliveredCredential {
7
+ export interface Unadmitted {
8
+ count: number;
9
+ overflow: boolean;
10
+ schemas?: {
11
+ schema: string;
12
+ reason: string;
13
+ }[];
14
+ objects?: {
15
+ object: string;
16
+ kind: string;
17
+ reason: string;
18
+ }[];
19
+ }
20
+ export interface DeliveredCredential {
8
21
  issuance_id: string;
9
22
  delivery_id: string;
23
+ access_profile?: string;
10
24
  uri: string;
25
+ unadmitted?: Unadmitted;
11
26
  connection_strings?: ConnectionStrings;
12
27
  connect_key_required?: boolean;
13
28
  }
@@ -15,12 +30,21 @@ export declare class CredentialIssueRefused extends Error {
15
30
  readonly code?: string | undefined;
16
31
  constructor(message: string, code?: string | undefined);
17
32
  }
33
+ export declare const ALL_SCHEMAS_PROFILE = "all_schemas_read_write_v1";
34
+ export declare const PUBLIC_READ_WRITE_PROFILE = "public_read_write_v1";
35
+ export type AccessProfile = typeof ALL_SCHEMAS_PROFILE | typeof PUBLIC_READ_WRITE_PROFILE;
36
+ export type CredentialRequest = {
37
+ sourceRole: string;
38
+ } | {
39
+ accessProfile: AccessProfile;
40
+ };
18
41
  export declare function validSourceRole(role: string): boolean;
19
42
  export declare function refusedBeforeIssuance(error: unknown): boolean;
20
- export declare function issueBranchCredential(token: string, branchID: string, requestID: string, sourceRole?: string): Promise<IssuedCredential>;
43
+ export declare function issueBranchCredential(token: string, branchID: string, requestID: string, credential: CredentialRequest): Promise<IssuedCredential>;
44
+ export declare function issueBranchCredentialWhenMarked(token: string, branchID: string, requestID: string, credential: CredentialRequest, say: (message: string) => void): Promise<IssuedCredential>;
21
45
  export declare function issuedConnectKey(issued: IssuedCredential): string | undefined;
22
46
  export declare function composeCredentialDelivery(delivered: DeliveredCredential, connectKey?: string): DeliveredCredential;
23
47
  export declare function waitForBranchCredential(token: string, issuanceID: string, stop?: AbortSignal, served?: Promise<void>): Promise<DeliveredCredential>;
48
+ export declare function describeUnadmitted(unadmitted: Unadmitted | undefined): string | undefined;
24
49
  export declare function acknowledgeBranchCredential(token: string, issuanceID: string, deliveryID: string): Promise<void>;
25
50
  export declare function revokeBranchCredential(token: string, issuanceID: string): Promise<void>;
26
- export {};
@@ -9,9 +9,29 @@ export class CredentialIssueRefused extends Error {
9
9
  this.code = code;
10
10
  }
11
11
  }
12
+ // branch_mark_pending is a branch registered ahead of its covering mark. The
13
+ // API refuses its credential before recording anything, and the same request
14
+ // is accepted once the mark binds (issueBranchCredentialWhenMarked).
15
+ const MARK_PENDING = 'branch_mark_pending';
16
+ // How long a credential wait lasts once nothing outside it bounds it: the wait
17
+ // for a branch to bind its covering mark, and the wait for delivery once the
18
+ // branch serves.
19
+ const WAIT_BOUND_MS = 10 * 60 * 1000;
12
20
  // The issuance refusals that come before any issuance is recorded: a request
13
21
  // refused with one of these left nothing to revoke.
14
- const sourceRoleIssueRefusals = new Set(['invalid_source_role', 'source_role_unsupported', 'source_role_held', 'source_role_revocation_pending', 'connect_key_unsupported']);
22
+ const issueRefusals = new Set(['invalid_source_role', 'source_role_unsupported', 'source_role_held', 'source_role_revocation_pending', 'connect_key_unsupported',
23
+ 'all_schemas_read_write_unsupported', 'all_schemas_read_write_unowned', MARK_PENDING]);
24
+ // The access profile branch connect asks for without --role, and the one an
25
+ // earlier release of this CLI asked for, which a recovery record naming no
26
+ // profile replays.
27
+ export const ALL_SCHEMAS_PROFILE = 'all_schemas_read_write_v1';
28
+ export const PUBLIC_READ_WRITE_PROFILE = 'public_read_write_v1';
29
+ // What branch connect says when the API refuses the profile it asks for. The
30
+ // CLI never retries with another profile: the user chooses one.
31
+ const profileRefusals = {
32
+ all_schemas_read_write_unsupported: 'This branch\'s environment runs an Ardent release that cannot issue a credential covering every schema. Upgrade the environment, or connect as a source role with --role (for example --role postgres).',
33
+ all_schemas_read_write_unowned: 'This branch was created without restoring its source\'s owners, so Ardent cannot tell which of its schemas belong to the database platform and will not issue a credential covering every schema. Connect as a source role with --role (for example --role postgres).',
34
+ };
15
35
  // sourceRolePattern is the name the API accepts for a source role: an
16
36
  // unquoted lower-case PostgreSQL identifier that begins with neither pg_ nor
17
37
  // ardent. The CLI checks it before it records or sends anything, so a mistyped
@@ -21,7 +41,7 @@ export function validSourceRole(role) {
21
41
  return sourceRolePattern.test(role);
22
42
  }
23
43
  export function refusedBeforeIssuance(error) {
24
- return error instanceof CredentialIssueRefused && error.code !== undefined && sourceRoleIssueRefusals.has(error.code);
44
+ return error instanceof CredentialIssueRefused && error.code !== undefined && issueRefusals.has(error.code);
25
45
  }
26
46
  function credentialPath(issuanceID) {
27
47
  return `/branch-credentials/${encodeURIComponent(issuanceID)}`;
@@ -40,15 +60,20 @@ function safeRequest(token, path, init) {
40
60
  throw new Error('Branch credential request failed. Check the API connection and try again.');
41
61
  });
42
62
  }
43
- // sourceRole, when given, asks for a fresh branch-only password on that
44
- // source role instead of an issued public read/write role. A replay of the same
45
- // request must name the same role.
46
- export async function issueBranchCredential(token, branchID, requestID, sourceRole) {
47
- const body = sourceRole === undefined
48
- ? { request_id: requestID, access_profile: 'public_read_write_v1' }
49
- : { request_id: requestID, source_role: sourceRole };
63
+ // A source role request asks for a fresh branch-only password on that source
64
+ // role; any other asks for an issued role under the access profile it names.
65
+ // A replay of the same request must name the same.
66
+ export async function issueBranchCredential(token, branchID, requestID, credential) {
67
+ const body = 'sourceRole' in credential
68
+ ? { request_id: requestID, source_role: credential.sourceRole }
69
+ : { request_id: requestID, access_profile: credential.accessProfile };
50
70
  const result = await safeRequest(token, `/branches/${encodeURIComponent(branchID)}/credentials`, {
51
71
  method: 'POST', body: JSON.stringify(body),
72
+ }).catch((error) => {
73
+ if (error instanceof CredentialIssueRefused && error.code !== undefined && profileRefusals[error.code] !== undefined) {
74
+ throw new CredentialIssueRefused(`${profileRefusals[error.code]} (${error.code})`, error.code);
75
+ }
76
+ throw error;
52
77
  });
53
78
  if (!result || typeof result.issuance_id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(result.issuance_id) || result.status !== 'pending') {
54
79
  throw new Error('Ardent API returned an invalid branch credential issuance.');
@@ -57,6 +82,33 @@ export async function issueBranchCredential(token, branchID, requestID, sourceRo
57
82
  // records and revokes an issuance even when its secret is absent or malformed.
58
83
  return { issuanceID: result.issuance_id, connectKey: result.connect_key, connectKeyRequired: result.connect_key_required };
59
84
  }
85
+ // issueBranchCredentialWhenMarked is issueBranchCredential for a branch that
86
+ // may still be waiting for its covering mark: a branch_mark_pending refusal
87
+ // sends the same request again on the poll cadence, saying so once, until the
88
+ // API accepts it or ten minutes pass. Every other refusal is thrown as it
89
+ // came. Credential recovery calls issueBranchCredential instead, since a
90
+ // branch_mark_pending answer already tells it there is nothing to revoke.
91
+ export async function issueBranchCredentialWhenMarked(token, branchID, requestID, credential, say) {
92
+ const deadline = Date.now() + WAIT_BOUND_MS;
93
+ const pause = pollPauser();
94
+ let announced = false;
95
+ for (;;) {
96
+ try {
97
+ return await issueBranchCredential(token, branchID, requestID, credential);
98
+ }
99
+ catch (error) {
100
+ if (!(error instanceof CredentialIssueRefused) || error.code !== MARK_PENDING)
101
+ throw error;
102
+ if (Date.now() >= deadline) {
103
+ throw new CredentialIssueRefused(`Timed out after ten minutes waiting for the branch's snapshot, so no credential was issued. Check branch status before trying again. ${error.message}`, MARK_PENDING);
104
+ }
105
+ if (!announced)
106
+ say('Waiting for the branch\'s snapshot...');
107
+ announced = true;
108
+ await pause();
109
+ }
110
+ }
111
+ }
60
112
  export function issuedConnectKey(issued) {
61
113
  if (issued.connectKeyRequired === undefined || issued.connectKeyRequired === false) {
62
114
  if (issued.connectKey !== undefined)
@@ -112,7 +164,7 @@ export function composeCredentialDelivery(delivered, connectKey) {
112
164
  // since the control plane delivers nothing before it, and until then the wait
113
165
  // is bounded by whatever bounds served.
114
166
  export async function waitForBranchCredential(token, issuanceID, stop, served) {
115
- const bound = 10 * 60 * 1000;
167
+ const bound = WAIT_BOUND_MS;
116
168
  let deadline = served ? Number.POSITIVE_INFINITY : Date.now() + bound;
117
169
  served?.then(() => { deadline = Date.now() + bound; }, () => { });
118
170
  // A 202 is re-read on the poll cadence; a 5xx or a lost response is retried
@@ -184,12 +236,47 @@ export async function waitForBranchCredential(token, issuanceID, stop, served) {
184
236
  DRIVERS.every((driver) => typeof strings[driver] === 'string' && strings[driver] !== '') &&
185
237
  strings.libpq === delivered.uri);
186
238
  if (delivered.issuance_id !== issuanceID || typeof delivered.delivery_id !== 'string' || !delivered.delivery_id ||
187
- typeof delivered.uri !== 'string' || !delivered.uri || !stringsValid)
239
+ typeof delivered.uri !== 'string' || !delivered.uri || !stringsValid ||
240
+ (delivered.access_profile !== undefined && typeof delivered.access_profile !== 'string') ||
241
+ (delivered.unadmitted !== undefined && !validUnadmitted(delivered.unadmitted)))
188
242
  throw new Error('Ardent API returned an invalid branch credential delivery.');
189
243
  return delivered;
190
244
  }
191
245
  throw new Error('Timed out waiting for a branch credential. Check branch status before trying again.');
192
246
  }
247
+ // validUnadmitted holds a delivered unadmitted list to its shape: a positive
248
+ // count, an overflow flag, and lists of non-empty strings when present, which
249
+ // together name no more than count.
250
+ function validUnadmitted(value) {
251
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
252
+ return false;
253
+ const list = value;
254
+ const strings = (entry, keys) => typeof entry === 'object' && entry !== null &&
255
+ keys.every((key) => typeof entry[key] === 'string' && entry[key] !== '');
256
+ const entries = (field, keys) => field === undefined || (Array.isArray(field) && field.every((entry) => strings(entry, keys)));
257
+ if (!Number.isSafeInteger(list.count) || list.count < 1 || typeof list.overflow !== 'boolean' ||
258
+ !entries(list.schemas, ['schema', 'reason']) || !entries(list.objects, ['object', 'kind', 'reason']))
259
+ return false;
260
+ const named = (list.schemas?.length ?? 0) + (list.objects?.length ?? 0);
261
+ return named <= list.count;
262
+ }
263
+ // describeUnadmitted is what branch connect says about a delivered
264
+ // credential's unadmitted list, or undefined when it leaves nothing out.
265
+ export function describeUnadmitted(unadmitted) {
266
+ if (!unadmitted)
267
+ return undefined;
268
+ const lines = [
269
+ ...(unadmitted.schemas ?? []).map((entry) => ` schema ${terminalText(entry.schema)}: ${terminalText(entry.reason)}`),
270
+ ...(unadmitted.objects ?? []).map((entry) => ` ${entry.kind === 'm' ? 'materialized view' : 'view'} ${terminalText(entry.object)}: ${terminalText(entry.reason)}`),
271
+ ];
272
+ const head = `This credential has no access to ${unadmitted.count} schema(s) or view(s) on the branch that it would otherwise cover`;
273
+ if (lines.length === 0) {
274
+ return unadmitted.overflow
275
+ ? `${head}; the list is too long to report by name.`
276
+ : `${head}; its node has not reported which yet. The branch's credential_unadmitted in the Ardent API names them once it has.`;
277
+ }
278
+ return `${head}:\n${lines.join('\n')}\nConnect with --role to reach them as a source role.`;
279
+ }
193
280
  export async function acknowledgeBranchCredential(token, issuanceID, deliveryID) {
194
281
  const deadline = Date.now() + 30_000;
195
282
  while (true) {
@@ -1,9 +1,10 @@
1
+ import { type CredentialRequest } from './credentials.js';
1
2
  export interface CredentialRecovery {
2
3
  file: string;
3
4
  branchID: string;
4
5
  requestID: string;
5
6
  issuanceID?: string;
6
- sourceRole?: string;
7
+ credential: CredentialRequest;
7
8
  }
8
9
  export declare class CredentialRecoveryCleanupError extends Error {
9
10
  readonly stage: 'remove' | 'sync';
@@ -13,7 +14,7 @@ export declare function openCredentialWitness(requestID: string): Promise<{
13
14
  port: number;
14
15
  close: () => Promise<void>;
15
16
  }>;
16
- export declare function rememberCredentialRequest(directory: string, branchID: string, token: string, port: number, requestID: string, sourceRole?: string): Promise<CredentialRecovery>;
17
+ export declare function rememberCredentialRequest(directory: string, branchID: string, token: string, port: number, requestID: string, credential: CredentialRequest): Promise<CredentialRecovery>;
17
18
  export declare function rememberCredentialIssuance(recovery: CredentialRecovery, issuanceID: string): Promise<void>;
18
19
  export declare function forgetCredentialRecovery(recovery: CredentialRecovery): Promise<void>;
19
20
  export declare function recoverBranchCredentials(directory: string, token: string): Promise<number>;
@@ -3,7 +3,11 @@ import { link, open, readFile, readdir, rename, unlink, writeFile } from 'node:f
3
3
  import { createConnection, createServer } from 'node:net';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { API_URL } from '../api/client.js';
6
- import { issueBranchCredential, refusedBeforeIssuance, revokeBranchCredential, validSourceRole } from './credentials.js';
6
+ import { ALL_SCHEMAS_PROFILE, issueBranchCredential, PUBLIC_READ_WRITE_PROFILE, refusedBeforeIssuance, revokeBranchCredential, validSourceRole } from './credentials.js';
7
+ // recordedRequest is the record's fields for what a request named.
8
+ function recordedRequest(credential) {
9
+ return 'sourceRole' in credential ? { source_role: credential.sourceRole } : { access_profile: credential.accessProfile };
10
+ }
7
11
  export class CredentialRecoveryCleanupError extends Error {
8
12
  stage;
9
13
  constructor(stage, cause) {
@@ -103,14 +107,14 @@ async function writeRecord(file, record, replace) {
103
107
  });
104
108
  }
105
109
  }
106
- export async function rememberCredentialRequest(directory, branchID, token, port, requestID, sourceRole) {
110
+ export async function rememberCredentialRequest(directory, branchID, token, port, requestID, credential) {
107
111
  const file = recoveryPath(directory, branchID, token, port, requestID);
108
- await writeRecord(file, { request_id: requestID, ...(sourceRole === undefined ? {} : { source_role: sourceRole }) }, false);
109
- return { file, branchID, requestID, sourceRole };
112
+ await writeRecord(file, { request_id: requestID, ...recordedRequest(credential) }, false);
113
+ return { file, branchID, requestID, credential };
110
114
  }
111
115
  export async function rememberCredentialIssuance(recovery, issuanceID) {
112
116
  recovery.issuanceID = issuanceID;
113
- await writeRecord(recovery.file, { request_id: recovery.requestID, issuance_id: issuanceID, ...(recovery.sourceRole === undefined ? {} : { source_role: recovery.sourceRole }) }, true);
117
+ await writeRecord(recovery.file, { request_id: recovery.requestID, issuance_id: issuanceID, ...recordedRequest(recovery.credential) }, true);
114
118
  }
115
119
  // forgetCredentialRecovery removes a record and syncs its directory. A record
116
120
  // already removed, by an earlier attempt whose sync failed, is removed; the
@@ -165,7 +169,8 @@ export async function recoverBranchCredentials(directory, token) {
165
169
  }
166
170
  if (!record || typeof record !== 'object' || record.request_id !== requestID ||
167
171
  (record.issuance_id !== undefined && (typeof record.issuance_id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(record.issuance_id))) ||
168
- (record.source_role !== undefined && (typeof record.source_role !== 'string' || !validSourceRole(record.source_role)))) {
172
+ (record.source_role !== undefined && (typeof record.source_role !== 'string' || !validSourceRole(record.source_role))) ||
173
+ (record.access_profile !== undefined && (record.source_role !== undefined || (record.access_profile !== ALL_SCHEMAS_PROFILE && record.access_profile !== PUBLIC_READ_WRITE_PROFILE)))) {
169
174
  failures.push(`Invalid branch credential recovery file ${name}.`);
170
175
  continue;
171
176
  }
@@ -180,12 +185,13 @@ export async function recoverBranchCredentials(directory, token) {
180
185
  failures.push(`Branch credential ${record.issuance_id ?? `request ${requestID}`} has a ${state === 'live' ? 'running session' : 'session whose state is unknown'}; finish or resolve that session before connecting again.`);
181
186
  continue;
182
187
  }
183
- const recovery = { file, branchID, requestID, issuanceID: record.issuance_id, sourceRole: record.source_role };
188
+ const credential = record.source_role !== undefined ? { sourceRole: record.source_role } : { accessProfile: record.access_profile ?? PUBLIC_READ_WRITE_PROFILE };
189
+ const recovery = { file, branchID, requestID, issuanceID: record.issuance_id, credential };
184
190
  try {
185
191
  if (!recovery.issuanceID) {
186
192
  let issuanceID;
187
193
  try {
188
- issuanceID = (await issueBranchCredential(token, branchID, requestID, recovery.sourceRole)).issuanceID;
194
+ issuanceID = (await issueBranchCredential(token, branchID, requestID, recovery.credential)).issuanceID;
189
195
  }
190
196
  catch (error) {
191
197
  // The API refuses a source role before it records an issuance and
@@ -908,5 +908,5 @@
908
908
  ]
909
909
  }
910
910
  },
911
- "version": "0.1.0"
911
+ "version": "0.1.2"
912
912
  }
package/openapi.json CHANGED
@@ -980,7 +980,7 @@
980
980
  "Branches"
981
981
  ],
982
982
  "summary": "Create branch",
983
- "description": "Requires environments.update in the connector organization. Uses the newest published snapshot and refuses an unavailable newest snapshot with 409 snapshot_unavailable. Caller-selected snapshots and other unknown fields are rejected. Names are unique within a connector while the branch is not deleted. One JSON object, at most 16 KiB. Poll operation.workflow_id; acceptance is not completion.",
983
+ "description": "Requires environments.update in the connector organization and a recent applied source position, or returns 409 source_progress_unavailable. Uses a published mark covering that position or accepts a branch waiting for an on-demand mark. A wait that does not bind a covering mark within 30 minutes fails the workflow as snapshot_unavailable and erases the reservation. Caller-selected snapshots and other unknown fields are rejected. Names are unique within a connector while the branch is not deleted. One JSON object, at most 16 KiB. Poll operation.workflow_id; acceptance is not completion.",
984
984
  "requestBody": {
985
985
  "required": true,
986
986
  "content": {
@@ -1662,6 +1662,19 @@
1662
1662
  "updated_at": {
1663
1663
  "type": "string",
1664
1664
  "format": "date-time"
1665
+ },
1666
+ "target_source_lsn": {
1667
+ "type": "string",
1668
+ "description": "Writer-applied source LSN accepted as the minimum covered position for this branch request. The materialized mark can have a later cutpoint. Absent on older branches."
1669
+ },
1670
+ "target_source_lineage_id": {
1671
+ "type": "string",
1672
+ "description": "Source lineage to which target_source_lsn belongs."
1673
+ },
1674
+ "target_source_observed_at": {
1675
+ "type": "string",
1676
+ "format": "date-time",
1677
+ "description": "When the authenticated node reported target_source_lsn."
1665
1678
  }
1666
1679
  }
1667
1680
  },
@@ -1901,7 +1914,7 @@
1901
1914
  "enum": [
1902
1915
  "seed_origin_v1"
1903
1916
  ],
1904
- "description": "Optional birth ownership restoration at the selected latest mark only. Returns 409 source_ownership_unavailable unless that mark matches a supported installation seed origin. Credentials retain public_read_write_v1."
1917
+ "description": "Optional birth ownership restoration at a published mark covering the accepted source position. An explicit seed_origin_v1 request is refused when that position is beyond the seed origin or its declaration is unavailable. Other mark-dependent refusals may fail the accepted workflow before materialization. Credentials retain public_read_write_v1."
1905
1918
  }
1906
1919
  },
1907
1920
  "additionalProperties": false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ardent-cli-beta",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "The Ardent command-line interface",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",