@mdbase-dev/connect 0.1.0-beta.93 → 0.1.0-beta.95

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.
@@ -2,6 +2,19 @@ import { validateGrantEncryption } from "./crypto.js";
2
2
  import { connectError, serverConnectError } from "./errors.js";
3
3
  import { canonicalJson } from "./operation-helpers.js";
4
4
  import { bytesToBase64Url, randomBase64Url } from "./base64.js";
5
+ /** Resolve bundled declarations and their stable storage identity without loading URLs. */
6
+ export function resolveManifestSource(input, redirect) {
7
+ const manifest = input instanceof URL ? input.href : input ?? defaultManifestSource();
8
+ const manifestSource = typeof manifest === "string"
9
+ ? manifest
10
+ : manifest.distribution === "portable"
11
+ ? `bundle:${manifest.id}:${manifestStorageFingerprint(manifest)}`
12
+ : `bundle:${manifest.id}`;
13
+ const redirectUri = redirect === undefined ? (typeof manifest !== "string" && manifest.distribution === "portable"
14
+ ? ""
15
+ : defaultRedirectUri()) : String(redirect);
16
+ return { manifest, manifestSource, redirectUri };
17
+ }
5
18
  /** Normalize fetch rejection at the exact I/O boundary. */
6
19
  export async function connectFetch(input, init, code, message) {
7
20
  try {
@@ -215,6 +228,7 @@ export function parseStored(value) {
215
228
  return null;
216
229
  }
217
230
  }
231
+ /** Parse both canonical and N-1 grant shapes so callers can diagnose legacy grants. */
218
232
  export function parseGrantScope(value) {
219
233
  if (!value || typeof value !== "object" || Array.isArray(value))
220
234
  return null;
@@ -246,6 +260,10 @@ export function parseGrantScope(value) {
246
260
  return null;
247
261
  return scope;
248
262
  }
263
+ /** Only this scope authorizes application data access. */
264
+ export function isCanonicalGrantScope(scope) {
265
+ return scope.access === "full_collection" && scope.contracts.length === 0;
266
+ }
249
267
  export function stripTrailingSlash(value) {
250
268
  return value.endsWith("/") ? value.slice(0, -1) : value;
251
269
  }
package/dist/session.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import type { CollectionOperation, ConnectProblem, JsonObject } from "@mdbase-dev/connect-protocol";
2
+ import { type MdbaseApplicationManifest, type MdbaseApplicationCapabilityId as ApplicationCapabilityId } from "./application-contract.js";
2
3
  import { MdbaseConnection, type MdbaseAuthorizationOutcome, type MdbaseAuthorizationResult, type MdbaseAuthorizeOptions } from "./connection.js";
3
4
  import type { MdbaseAuthorizationCapabilities, MdbaseConnectionInfo } from "./connection-types.js";
4
- import { type AuthorizationProblemCode, type ConnectOutcome, type SessionProblemCode } from "./outcomes.js";
5
+ import { type AuthorizationProblemCode, type RegistrationProblemCode, type ConnectOutcome, type SessionProblemCode } from "./outcomes.js";
5
6
  import type { MdbaseSelectionHistory, MdbaseApplicationSelection } from "./selection.js";
6
7
  import type { ConnectRequestOptions } from "./operation-types.js";
7
8
  export interface MdbaseSessionConnect<Frontmatter extends JsonObject> {
9
+ manifest?(options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseApplicationManifest, RegistrationProblemCode>>;
8
10
  authorize(options?: MdbaseAuthorizeOptions): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter>, AuthorizationProblemCode>>;
9
11
  completeAuthorization(callbackUrl: string, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseAuthorizationResult<Frontmatter>, AuthorizationProblemCode>>;
10
12
  connections(): MdbaseConnectionInfo[];
@@ -17,7 +19,7 @@ export interface MdbaseSessionOptions {
17
19
  operations?: CollectionOperation[];
18
20
  autoSelect?: "only" | "never";
19
21
  }
20
- export type MdbaseUnavailableReason = "not_authorized" | "authorization_lost" | "invalid_stored_grant";
22
+ export type MdbaseUnavailableReason = "not_authorized" | "authorization_lost" | "invalid_stored_grant" | "legacy_scope_reauthorization_required";
21
23
  export type MdbaseSessionSnapshot<Frontmatter extends JsonObject = JsonObject> = {
22
24
  status: "not_started";
23
25
  connections: MdbaseConnectionInfo[];
@@ -87,11 +89,16 @@ export declare class MdbaseSession<Frontmatter extends JsonObject = JsonObject>
87
89
  forget(collectionId: string): ConnectOutcome<void, SessionProblemCode>;
88
90
  authorize(target: "choose" | "selected" | {
89
91
  collectionId: string;
90
- }, options?: Omit<MdbaseAuthorizeOptions, "operations" | "returnTo" | "target">): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter>, SessionProblemCode>>;
91
- ensureOperations(requiredOperations: CollectionOperation[], options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter> | {
92
+ }, options?: Omit<MdbaseAuthorizeOptions, "returnTo" | "target">): Promise<ConnectOutcome<MdbaseAuthorizationOutcome<Frontmatter>, SessionProblemCode>>;
93
+ ensureOperations(requiredOperations: CollectionOperation[], options?: ConnectRequestOptions): Promise<ConnectOutcome<{
92
94
  kind: "unchanged";
93
95
  connection: MdbaseConnection<Frontmatter>;
94
- }, SessionProblemCode>>;
96
+ } | MdbaseAuthorizationOutcome<Frontmatter>, SessionProblemCode>>;
97
+ ensureCapabilities(requiredCapabilities: ApplicationCapabilityId[], options?: ConnectRequestOptions): Promise<ConnectOutcome<{
98
+ kind: "unchanged";
99
+ connection: MdbaseConnection<Frontmatter>;
100
+ } | MdbaseAuthorizationOutcome<Frontmatter>, SessionProblemCode>>;
101
+ private ensureAuthorization;
95
102
  handleAuthorizationCallback(callbackUrl: string, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseConnection<Frontmatter>, SessionProblemCode>>;
96
103
  private completeAuthorizationCallback;
97
104
  private beginCallbackCompletion;
package/dist/session.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { APPLICATION_CAPABILITY_DEFINITIONS, capabilityOperationsForContractVersion } from "@mdbase-dev/connect-protocol";
2
+ import { retainedCapabilities, semanticContractVersion, validateAuthorizationSelection } from "./application-contract.js";
1
3
  import { authorizationCallbackState, authorizationReturnToFromProblem, isAuthorizationCallbackUrl } from "./authorization-url.js";
2
4
  import { MdbaseConnectError, connectProblem } from "./errors.js";
3
5
  import { DEFAULT_OPERATIONS } from "./internal-types.js";
@@ -221,9 +223,16 @@ export class MdbaseSession {
221
223
  const operation = this.beginLifecycleOperation(options);
222
224
  this.transactionDepth += 1;
223
225
  try {
226
+ const declaration = await this.connect.manifest?.(operation.options);
227
+ if (declaration && !declaration.ok)
228
+ return declaration;
229
+ if (!this.lifecycleCurrent(operation.generation))
230
+ return this.destroyedOutcome(connectSuccess(undefined));
231
+ const version = declaration ? semanticContractVersion(declaration.value.requirements) : 1;
224
232
  const outcome = await this.connect.authorize({
225
233
  ...operation.options,
226
- operations: this.operations,
234
+ ...(version === 1 && options.operations === undefined && options.capabilities === undefined
235
+ ? { operations: this.operations } : {}),
227
236
  target: targetCollectionId === null
228
237
  ? { kind: "choose" }
229
238
  : { kind: "collection", collectionId: targetCollectionId },
@@ -242,7 +251,13 @@ export class MdbaseSession {
242
251
  this.refresh();
243
252
  }
244
253
  }
245
- async ensureOperations(requiredOperations, options) {
254
+ ensureOperations(requiredOperations, options) {
255
+ return this.ensureAuthorization({ operations: requiredOperations }, options);
256
+ }
257
+ ensureCapabilities(requiredCapabilities, options) {
258
+ return this.ensureAuthorization({ capabilities: requiredCapabilities }, options);
259
+ }
260
+ async ensureAuthorization(selection, options) {
246
261
  const lifecycle = this.lifecycleProblem();
247
262
  if (lifecycle)
248
263
  return connectFailure(lifecycle);
@@ -250,17 +265,30 @@ export class MdbaseSession {
250
265
  if (current.status !== "ready") {
251
266
  return connectFailure(connectProblem("collection_not_ready", "Choose an authorized collection first."));
252
267
  }
253
- const required = uniqueOperations(requiredOperations);
254
- const capabilities = current.connection.authorizationCapabilities(required);
255
- if (capabilities.sufficient) {
256
- return connectSuccess({ kind: "unchanged", connection: current.connection });
257
- }
258
268
  const operation = this.beginLifecycleOperation(options);
259
269
  this.transactionDepth += 1;
260
270
  try {
271
+ const declaration = await this.connect.manifest?.(operation.options);
272
+ if (declaration && !declaration.ok)
273
+ return declaration;
274
+ if (!this.lifecycleCurrent(operation.generation))
275
+ return this.destroyedOutcome(connectSuccess(undefined));
276
+ // Custom advanced session adapters predating manifest() retain exact v1
277
+ // operations. The concrete SDK always supplies its declared manifest.
278
+ const requirements = declaration?.value.requirements ?? ("operations" in selection
279
+ ? { access: "full_collection", contracts: [] }
280
+ : { access: "full_collection", contracts: [], capabilities: { contract_version: 2, required: [], optional: Object.keys(APPLICATION_CAPABILITY_DEFINITIONS) } });
281
+ validateAuthorizationSelection(requirements, { ...options, ...selection });
282
+ const requiredOperations = "operations" in selection ? selection.operations : selection.capabilities.flatMap((id) => capabilityOperationsForContractVersion(semanticContractVersion(requirements), id) ?? []);
283
+ const authorization = current.connection.authorizationCapabilities(requiredOperations);
284
+ if (authorization.sufficient)
285
+ return connectSuccess({ kind: "unchanged", connection: current.connection });
286
+ const incremental = "operations" in selection || semanticContractVersion(requirements) === 1
287
+ ? { operations: uniqueOperations([...authorization.grantedOperations, ...requiredOperations]) }
288
+ : { capabilities: [...new Set([...retainedCapabilities(requirements, authorization.grantedOperations), ...selection.capabilities])] };
261
289
  const outcome = await this.connect.authorize({
262
290
  ...operation.options,
263
- operations: uniqueOperations([...capabilities.grantedOperations, ...capabilities.missingOperations]),
291
+ ...incremental,
264
292
  target: { kind: "collection", collectionId: current.collectionId },
265
293
  returnTo: this.selection.authorizationReturnTo()
266
294
  });
@@ -271,6 +299,11 @@ export class MdbaseSession {
271
299
  }
272
300
  return outcome;
273
301
  }
302
+ catch (error) {
303
+ if (error instanceof MdbaseConnectError)
304
+ return connectFailure(error.problem);
305
+ throw error;
306
+ }
274
307
  finally {
275
308
  operation.dispose();
276
309
  this.transactionDepth -= 1;
@@ -3,7 +3,7 @@ interface ReadStoredTokenOptions {
3
3
  stored: string | null;
4
4
  collectionId: string;
5
5
  relayEncryption: "required" | "disabled";
6
- invalidate(keyHandle?: string): void;
6
+ invalidate(keyHandle?: string, reason?: "invalid_stored_grant" | "legacy_scope_reauthorization_required"): void;
7
7
  directCapable(token: StoredToken): boolean;
8
8
  }
9
9
  export declare function readStoredToken({ stored, collectionId, relayEncryption, invalidate, directCapable }: ReadStoredTokenOptions): StoredToken | null;
@@ -1,8 +1,8 @@
1
- import { parseGrantScope, parseStored, validFileCapability, validStoredAuthority, validStoredEncryption } from "./runtime-utils.js";
1
+ import { isCanonicalGrantScope, parseGrantScope, parseStored, validFileCapability, validStoredAuthority, validStoredEncryption } from "./runtime-utils.js";
2
2
  export function readStoredToken({ stored, collectionId, relayEncryption, invalidate, directCapable }) {
3
3
  const token = parseStored(stored);
4
- const reject = (keyHandle) => {
5
- invalidate(typeof keyHandle === "string" ? keyHandle : undefined);
4
+ const reject = (keyHandle, reason = "invalid_stored_grant") => {
5
+ invalidate(typeof keyHandle === "string" ? keyHandle : undefined, reason);
6
6
  return null;
7
7
  };
8
8
  if (!token) {
@@ -27,8 +27,12 @@ export function readStoredToken({ stored, collectionId, relayEncryption, invalid
27
27
  || (token.refreshExpiresAt !== undefined
28
28
  && (typeof token.refreshExpiresAt !== "number" || !Number.isFinite(token.refreshExpiresAt))))
29
29
  return reject(token.keyHandle);
30
- if (!parseGrantScope(token.scope))
30
+ const scope = parseGrantScope(token.scope);
31
+ if (!scope)
31
32
  return reject(token.keyHandle);
33
+ if (!isCanonicalGrantScope(scope)) {
34
+ return reject(token.keyHandle, "legacy_scope_reauthorization_required");
35
+ }
32
36
  if (token.fileCapability && !validFileCapability(token.fileCapability)) {
33
37
  return reject(token.keyHandle);
34
38
  }
@@ -0,0 +1,77 @@
1
+ import type { ContractRequirement, FileAction, JsonObject } from "@mdbase-dev/connect-protocol";
2
+ import type { MdbaseApplicationManifest } from "./application-contract.js";
3
+ import type { MdbaseConnectionInfo } from "./connection-types.js";
4
+ import { type MdbaseEffectiveCapabilities, type MdbaseCapabilityEvidence } from "./capabilities.js";
5
+ import type { MdbaseConnection } from "./connection.js";
6
+ import type { ConnectRequestOptions } from "./operation-types.js";
7
+ type ReadinessPublication = {
8
+ status: "authorization_required";
9
+ } | {
10
+ status: "blocked";
11
+ problem: {
12
+ code: string;
13
+ message: string;
14
+ };
15
+ };
16
+ /** Live verification owns no session state; callers supply identity and cancellation guards. */
17
+ export declare function verifyStructuredReadiness<Frontmatter extends JsonObject>(input: {
18
+ collectionId: string;
19
+ readiness: MdbaseStructuredReadiness;
20
+ contracts: ContractRequirement[];
21
+ connection: MdbaseConnection<Frontmatter>;
22
+ controller: AbortController;
23
+ options?: ConnectRequestOptions;
24
+ requestMs: number | null;
25
+ isCurrent: () => boolean;
26
+ publish: (result: ReadinessPublication) => void;
27
+ }): Promise<boolean>;
28
+ export declare function publishedStructuredReadiness(source: MdbaseStructuredReadiness, status: string, verification?: "cached" | "verified"): MdbaseStructuredReadiness;
29
+ export interface ApplicationSessionContext {
30
+ collectionId: string;
31
+ info: MdbaseConnectionInfo;
32
+ capabilities: MdbaseEffectiveCapabilities;
33
+ /** Present only for semantic contract v2. */
34
+ readiness?: MdbaseStructuredReadiness;
35
+ connections: MdbaseConnectionInfo[];
36
+ }
37
+ export declare function applicationReadinessContext(manifest: MdbaseApplicationManifest, collectionId: string, info: MdbaseConnectionInfo, connections: MdbaseConnectionInfo[]): ApplicationSessionContext;
38
+ export declare function verificationKey(manifest: MdbaseApplicationManifest, collectionId: string): string;
39
+ export declare function verificationValue(manifest: MdbaseApplicationManifest): string;
40
+ export declare function collectionSetupInput(manifest: MdbaseApplicationManifest, applicationId: string, declarationDigest: string, typePackAdoptions?: Record<string, Record<string, string>>): {
41
+ applicationId: string;
42
+ declarationDigest: string;
43
+ requirements: {
44
+ configuration: import("@mdbase-dev/connect-protocol").ConfigurationRequirement[];
45
+ };
46
+ provisions: {
47
+ configuration: import("@mdbase-dev/connect-protocol").ConfigurationProvision[];
48
+ typePacks: import("@mdbase-dev/connect-protocol").TypePackProvision[];
49
+ };
50
+ typePackAdoptions?: Record<string, Record<string, string>> | undefined;
51
+ };
52
+ /** Independent v2 declarations; never legacy capability aliases. */
53
+ export interface MdbaseStructuredReadiness {
54
+ files: {
55
+ state: "not_required" | "available" | "requires_authorization";
56
+ missingRequiredActions: FileAction[];
57
+ missingOptionalActions: FileAction[];
58
+ scopeMatches: boolean;
59
+ evidence: MdbaseCapabilityEvidence[];
60
+ };
61
+ contracts: {
62
+ state: "not_required" | "checking" | "verified" | "requires_setup" | "requires_authorization" | "temporarily_unavailable";
63
+ missing: ContractRequirement[];
64
+ evidence: MdbaseCapabilityEvidence[];
65
+ };
66
+ setup: {
67
+ state: "not_required" | "checking" | "review_required" | "current" | "cached" | "blocked" | "requires_authorization";
68
+ evidence: MdbaseCapabilityEvidence[];
69
+ };
70
+ notifications: {
71
+ state: "not_required" | "requires_registration";
72
+ registration: "unverified";
73
+ evidence: MdbaseCapabilityEvidence[];
74
+ };
75
+ }
76
+ export declare function structuredReadiness(manifest: MdbaseApplicationManifest, info: MdbaseConnectionInfo): MdbaseStructuredReadiness;
77
+ export {};
@@ -0,0 +1,126 @@
1
+ /* The versioned protocol package is the sole capability-to-operation compiler. */
2
+ import { effectiveCapabilities } from "./capabilities.js";
3
+ import { MdbaseConnectError } from "./errors.js";
4
+ import { withRequestBudget } from "./request-budget.js";
5
+ /** Live verification owns no session state; callers supply identity and cancellation guards. */
6
+ export async function verifyStructuredReadiness(input) {
7
+ const { collectionId, readiness, contracts, connection, controller, options, requestMs, isCurrent, publish } = input;
8
+ const abort = () => controller.abort(options?.signal?.reason);
9
+ if (options?.signal?.aborted)
10
+ abort();
11
+ else
12
+ options?.signal?.addEventListener("abort", abort, { once: true });
13
+ try {
14
+ const outcome = await withRequestBudget({ ...options, signal: controller.signal }, requestMs, request => connection.describe(request));
15
+ if (!isCurrent())
16
+ return false;
17
+ if (!outcome.ok) {
18
+ const authorizationRequired = ["access_denied", "collection_access_denied", "insufficient_access", "not_authorized", "authorization_expired", "application_declaration_mismatch"].includes(outcome.problem.code);
19
+ readiness.contracts = { state: authorizationRequired ? "requires_authorization" : "temporarily_unavailable", missing: [], evidence: [{ source: "runtime", fact: outcome.problem.message }] };
20
+ publish(authorizationRequired
21
+ ? { status: "authorization_required" }
22
+ : { status: "blocked", problem: outcome.problem });
23
+ return false;
24
+ }
25
+ if (outcome.value.collectionId !== collectionId) {
26
+ readiness.contracts = { state: "temporarily_unavailable", missing: [], evidence: [] };
27
+ publish({ status: "blocked", problem: { code: "collection_not_ready", message: "Description belongs to a different collection." } });
28
+ return false;
29
+ }
30
+ const missing = contracts.filter(required => !outcome.value.contracts.some(actual => actual.id === required.id && actual.version === required.version && actual.digest === required.digest));
31
+ readiness.contracts = { state: missing.length ? "requires_setup" : "verified", missing,
32
+ evidence: [{ source: "authority", fact: "Exact contract ID, version and digest checked by live describe for this collection." }] };
33
+ if (missing.length) {
34
+ publish({ status: "blocked", problem: { code: "collection_not_ready", message: "The collection is missing required exact contracts." } });
35
+ return false;
36
+ }
37
+ }
38
+ catch (error) {
39
+ if (!isCurrent())
40
+ return false;
41
+ if (!(error instanceof MdbaseConnectError))
42
+ throw error;
43
+ readiness.contracts = { state: "temporarily_unavailable", missing: [], evidence: [{ source: "runtime", fact: error.problem.message }] };
44
+ publish({ status: "blocked", problem: error.problem });
45
+ return false;
46
+ }
47
+ finally {
48
+ options?.signal?.removeEventListener("abort", abort);
49
+ }
50
+ return true;
51
+ }
52
+ export function publishedStructuredReadiness(source, status, verification) {
53
+ const readiness = structuredClone(source);
54
+ if (readiness.setup.state !== "not_required" && readiness.setup.state !== "current") {
55
+ readiness.setup.state = status === "ready" ? (verification === "cached" ? "cached" : "current")
56
+ : status === "setup_review_required" ? "review_required"
57
+ : status === "authorization_required" ? "requires_authorization"
58
+ : status === "blocked" ? "blocked" : "checking";
59
+ readiness.setup.evidence = [{ source: status === "ready" && verification === "cached" ? "runtime" : "authority",
60
+ fact: `Collection setup verification: ${readiness.setup.state}.` }];
61
+ }
62
+ return readiness;
63
+ }
64
+ export function applicationReadinessContext(manifest, collectionId, info, connections) {
65
+ return {
66
+ collectionId, info,
67
+ capabilities: effectiveCapabilities(manifest.requirements.capabilities, manifest, info),
68
+ ...(manifest.requirements?.capabilities?.contract_version === 2
69
+ ? { readiness: structuredReadiness(manifest, info) } : {}),
70
+ connections
71
+ };
72
+ }
73
+ export function verificationKey(manifest, collectionId) {
74
+ return `mdbase-application-session:v1:${manifest.id}:${collectionId}`;
75
+ }
76
+ export function verificationValue(manifest) {
77
+ return JSON.stringify({
78
+ capabilities: manifest.requirements?.capabilities,
79
+ requirements: manifest.requirements?.configuration ?? [],
80
+ provisions: manifest.provisions ?? {}
81
+ });
82
+ }
83
+ export function collectionSetupInput(manifest, applicationId, declarationDigest, typePackAdoptions) {
84
+ return {
85
+ applicationId, declarationDigest,
86
+ requirements: { configuration: manifest.requirements?.configuration ?? [] },
87
+ provisions: {
88
+ configuration: manifest.provisions?.configuration ?? [],
89
+ typePacks: manifest.provisions?.type_packs ?? []
90
+ },
91
+ ...(typePackAdoptions ? { typePackAdoptions } : {})
92
+ };
93
+ }
94
+ function sameScope(left, right) {
95
+ if (!right || left.kind !== right.kind)
96
+ return false;
97
+ if (left.kind === "collection" || right.kind === "collection")
98
+ return true;
99
+ const a = [...new Set(left.folders)].sort();
100
+ const b = [...new Set(right.folders)].sort();
101
+ return JSON.stringify(a) === JSON.stringify(b);
102
+ }
103
+ export function structuredReadiness(manifest, info) {
104
+ const files = manifest.requirements?.files;
105
+ const requirement = files && "required" in files ? files : undefined;
106
+ const scopeMatches = !requirement || sameScope(requirement.scope, info.fileCapability?.scope);
107
+ const missing = (actions) => actions.filter(action => !scopeMatches || !info.fileCapability?.actions.includes(action));
108
+ const missingRequiredActions = missing(requirement?.required ?? []);
109
+ const missingOptionalActions = missing(requirement?.optional ?? []);
110
+ const contracts = manifest.requirements?.contracts ?? [];
111
+ const setup = (manifest.provisions?.type_packs.length ?? 0) > 0 || (manifest.provisions?.configuration?.length ?? 0) > 0;
112
+ return {
113
+ files: {
114
+ state: !requirement ? "not_required" : missingRequiredActions.length ? "requires_authorization" : "available",
115
+ missingRequiredActions, missingOptionalActions, scopeMatches,
116
+ evidence: [{ source: "authorization", fact: "Compared exact file actions and folder scope with connection.info().fileCapability." }]
117
+ },
118
+ contracts: { state: contracts.length ? "checking" : "not_required", missing: [], evidence: [] },
119
+ setup: { state: setup ? "checking" : "not_required", evidence: [] },
120
+ notifications: {
121
+ state: manifest.notifications?.criteria.length ? "requires_registration" : "not_required",
122
+ registration: "unverified",
123
+ evidence: [{ source: "runtime", fact: "The connection API exposes registration commands but no current delivery-registration query. Manifest criteria do not prove approval or delivery registration." }]
124
+ }
125
+ };
126
+ }
@@ -1,6 +1,6 @@
1
1
  import { validateGrantEncryption } from "./crypto.js";
2
2
  import { connectError } from "./errors.js";
3
- import { parseGrantScope, validAuthorityTokenResponse, validFileCapability } from "./runtime-utils.js";
3
+ import { isCanonicalGrantScope, parseGrantScope, validAuthorityTokenResponse, validFileCapability } from "./runtime-utils.js";
4
4
  export function storedTokenFromResponse({ body, clientId, keyHandle, previous, retiredKeyHandles, defaultApplicationOrigin, pinConnectorIdentity }) {
5
5
  const collectionId = body.collection_id;
6
6
  if (typeof collectionId !== "string") {
@@ -10,6 +10,9 @@ export function storedTokenFromResponse({ body, clientId, keyHandle, previous, r
10
10
  if (!scope) {
11
11
  throw connectError("invalid_token_response", "Authorization returned no valid collection scope.");
12
12
  }
13
+ if (!isCanonicalGrantScope(scope)) {
14
+ throw connectError("legacy_scope_reauthorization_required", "Authorization returned a legacy contract-scoped grant. Reauthorize for the entire collection.");
15
+ }
13
16
  if (body.authority && !validAuthorityTokenResponse(body.authority, collectionId)) {
14
17
  throw connectError("invalid_token_response", "Authorization returned an invalid remote authority capability.");
15
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdbase-dev/connect",
3
- "version": "0.1.0-beta.93",
3
+ "version": "0.1.0-beta.95",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "README.md"
34
34
  ],
35
35
  "dependencies": {
36
- "@mdbase-dev/connect-protocol": "0.1.0-beta.93"
36
+ "@mdbase-dev/connect-protocol": "0.1.0-beta.95"
37
37
  },
38
38
  "devDependencies": {
39
39
  "esbuild": "^0.28.1",