@mdbase-dev/connect-protocol 0.1.0-beta.22 → 0.1.0-beta.24

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.
@@ -0,0 +1,30 @@
1
+ import type { ApplicationFileRequirement } from "./files.js";
2
+ import type { CollectionOperation } from "./operations.js";
3
+ export type ApplicationAuthorizationFlow = "authorization_code" | "device_code";
4
+ export interface ApplicationAuthorizationBinding {
5
+ protocol_version: 1;
6
+ authorization_id: string;
7
+ application_id: string;
8
+ application_manifest_digest: string;
9
+ application_installation_id: string;
10
+ installation_agreement_public_key: string;
11
+ installation_signing_public_key: string;
12
+ grant_agreement_public_key: string;
13
+ grant_signing_public_key: string;
14
+ flow: ApplicationAuthorizationFlow;
15
+ authorization_nonce: string;
16
+ issued_at: string;
17
+ expires_at: string;
18
+ redirect_uri?: string;
19
+ state?: string;
20
+ code_challenge: string;
21
+ requested_operations: CollectionOperation[];
22
+ requested_files?: ApplicationFileRequirement;
23
+ collection_id?: string;
24
+ }
25
+ export interface ApplicationAuthorizationProof {
26
+ binding: ApplicationAuthorizationBinding;
27
+ signature: string;
28
+ }
29
+ export declare function applicationInstallationIdFromPublicKeys(agreementPublicKey: string, signingPublicKey: string): Promise<string>;
30
+ export declare function authorizationSigningMessage(binding: ApplicationAuthorizationBinding): Uint8Array;
@@ -0,0 +1,170 @@
1
+ const INSTALLATION_ID_DOMAIN = new TextEncoder().encode("mdbase-connect application installation id v1\0");
2
+ const AUTHORIZATION_PROOF_DOMAIN = new TextEncoder().encode("mdbase-connect application authorization proof\0");
3
+ export async function applicationInstallationIdFromPublicKeys(agreementPublicKey, signingPublicKey) {
4
+ const agreement = publicKey(agreementPublicKey);
5
+ const signing = publicKey(signingPublicKey);
6
+ if (equalBytes(agreement, signing)) {
7
+ throw new Error("Application installation agreement and signing keys must be distinct.");
8
+ }
9
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", concat([INSTALLATION_ID_DOMAIN, field(agreement), field(signing)])));
10
+ const id = digest.slice(0, 16);
11
+ id[6] = (id[6] & 0x0f) | 0x80;
12
+ id[8] = (id[8] & 0x3f) | 0x80;
13
+ return [
14
+ hex(id.slice(0, 4)),
15
+ hex(id.slice(4, 6)),
16
+ hex(id.slice(6, 8)),
17
+ hex(id.slice(8, 10)),
18
+ hex(id.slice(10, 16))
19
+ ].join("-");
20
+ }
21
+ export function authorizationSigningMessage(binding) {
22
+ const keys = [
23
+ publicKey(binding.installation_agreement_public_key),
24
+ publicKey(binding.installation_signing_public_key),
25
+ publicKey(binding.grant_agreement_public_key),
26
+ publicKey(binding.grant_signing_public_key)
27
+ ];
28
+ if (keys.some((key, index) => keys.slice(index + 1).some((other) => equalBytes(key, other)))) {
29
+ throw new Error("Application authorization keys must be distinct.");
30
+ }
31
+ const nonce = canonicalBase64(binding.authorization_nonce);
32
+ const challenge = canonicalBase64(binding.code_challenge);
33
+ if (binding.protocol_version !== 1
34
+ || nonce.byteLength !== 32
35
+ || !/^[0-9a-f]{64}$/u.test(binding.application_manifest_digest)
36
+ || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/u.test(binding.issued_at)
37
+ || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/u.test(binding.expires_at)
38
+ || challenge.byteLength !== 32
39
+ || (binding.requested_operations.length === 0 && binding.requested_files === undefined)
40
+ || new Set(binding.requested_operations).size !== binding.requested_operations.length
41
+ || binding.requested_operations.some((operation) => !operation || operation.includes("\0"))
42
+ || (binding.flow === "authorization_code"
43
+ ? binding.redirect_uri === undefined || binding.state === undefined
44
+ : binding.flow === "device_code"
45
+ ? binding.redirect_uri !== undefined || binding.state !== undefined
46
+ : true)) {
47
+ throw new Error("Application authorization binding is invalid.");
48
+ }
49
+ validateRequestedFiles(binding.requested_files);
50
+ const operationFields = binding.requested_operations.map((operation) => field(new TextEncoder().encode(operation)));
51
+ return concat([
52
+ AUTHORIZATION_PROOF_DOMAIN,
53
+ u32(binding.protocol_version),
54
+ field(uuidBytes(binding.application_id)),
55
+ field(uuidBytes(binding.authorization_id)),
56
+ field(hexBytes(binding.application_manifest_digest)),
57
+ field(uuidBytes(binding.application_installation_id)),
58
+ ...keys.map(field),
59
+ field(new TextEncoder().encode(binding.flow)),
60
+ field(nonce),
61
+ field(new TextEncoder().encode(binding.issued_at)),
62
+ field(new TextEncoder().encode(binding.expires_at)),
63
+ optionalString(binding.redirect_uri),
64
+ optionalString(binding.state),
65
+ field(new TextEncoder().encode(binding.code_challenge)),
66
+ u32(operationFields.length),
67
+ ...operationFields,
68
+ requestedFiles(binding.requested_files),
69
+ optionalUuid(binding.collection_id)
70
+ ]);
71
+ }
72
+ function validateRequestedFiles(files) {
73
+ if (files === undefined)
74
+ return;
75
+ if (files.actions.length === 0
76
+ || new Set(files.actions).size !== files.actions.length
77
+ || (files.scope.kind === "selected_folders"
78
+ && (files.scope.folders.length === 0
79
+ || new Set(files.scope.folders).size !== files.scope.folders.length
80
+ || files.scope.folders.some((folder) => !folder || folder.includes("\0"))))) {
81
+ throw new Error("Application authorization file request is invalid.");
82
+ }
83
+ }
84
+ function requestedFiles(files) {
85
+ if (files === undefined)
86
+ return new Uint8Array([0]);
87
+ const actions = files.actions.map((action) => field(new TextEncoder().encode(action)));
88
+ return concat([
89
+ new Uint8Array([1]),
90
+ u32(actions.length),
91
+ ...actions,
92
+ field(new TextEncoder().encode(files.scope.kind)),
93
+ ...(files.scope.kind === "selected_folders"
94
+ ? [
95
+ u32(files.scope.folders.length),
96
+ ...files.scope.folders.map((folder) => field(new TextEncoder().encode(folder)))
97
+ ]
98
+ : [])
99
+ ]);
100
+ }
101
+ function optionalString(value) {
102
+ return value === undefined
103
+ ? new Uint8Array([0])
104
+ : concat([new Uint8Array([1]), field(new TextEncoder().encode(value))]);
105
+ }
106
+ function optionalUuid(value) {
107
+ return value === undefined
108
+ ? new Uint8Array([0])
109
+ : concat([new Uint8Array([1]), field(uuidBytes(value))]);
110
+ }
111
+ function publicKey(value) {
112
+ const key = canonicalBase64(value);
113
+ if (key.byteLength !== 65 || key[0] !== 4) {
114
+ throw new Error("Application authorization public key is invalid.");
115
+ }
116
+ return key;
117
+ }
118
+ function canonicalBase64(value) {
119
+ if (!/^[A-Za-z0-9_-]+$/u.test(value))
120
+ throw new Error("Invalid base64url value.");
121
+ const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
122
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
123
+ const decoded = Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
124
+ if (base64Url(decoded) !== value)
125
+ throw new Error("Non-canonical base64url value.");
126
+ return decoded;
127
+ }
128
+ function uuidBytes(value) {
129
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(value)) {
130
+ throw new Error("Invalid UUID value.");
131
+ }
132
+ const compact = value.replaceAll("-", "");
133
+ return hexBytes(compact);
134
+ }
135
+ function hexBytes(value) {
136
+ if (!/^[0-9a-f]+$/u.test(value) || value.length % 2 !== 0) {
137
+ throw new Error("Invalid hexadecimal value.");
138
+ }
139
+ return Uint8Array.from({ length: value.length / 2 }, (_, index) => Number.parseInt(value.slice(index * 2, index * 2 + 2), 16));
140
+ }
141
+ function field(value) {
142
+ return concat([u32(value.byteLength), value]);
143
+ }
144
+ function u32(value) {
145
+ const output = new Uint8Array(4);
146
+ new DataView(output.buffer).setUint32(0, value, false);
147
+ return output;
148
+ }
149
+ function concat(values) {
150
+ const output = new Uint8Array(values.reduce((length, value) => length + value.byteLength, 0));
151
+ let offset = 0;
152
+ for (const value of values) {
153
+ output.set(value, offset);
154
+ offset += value.byteLength;
155
+ }
156
+ return output;
157
+ }
158
+ function base64Url(value) {
159
+ return btoa(String.fromCharCode(...value))
160
+ .replaceAll("+", "-")
161
+ .replaceAll("/", "_")
162
+ .replace(/=+$/u, "");
163
+ }
164
+ function equalBytes(left, right) {
165
+ return left.byteLength === right.byteLength
166
+ && left.every((value, index) => value === right[index]);
167
+ }
168
+ function hex(value) {
169
+ return [...value].map((byte) => byte.toString(16).padStart(2, "0")).join("");
170
+ }
@@ -152,6 +152,10 @@ export declare const CONNECT_PROBLEM_CATALOG: {
152
152
  readonly category: "conflict";
153
153
  readonly recovery: "retry";
154
154
  };
155
+ readonly first_contact_handler_required: {
156
+ readonly category: "compatibility";
157
+ readonly recovery: "upgrade_application";
158
+ };
155
159
  readonly hosted_provider_unavailable: {
156
160
  readonly category: "availability";
157
161
  readonly recovery: "retry";
@@ -176,6 +180,10 @@ export declare const CONNECT_PROBLEM_CATALOG: {
176
180
  readonly category: "integrity";
177
181
  readonly recovery: "contact_support";
178
182
  };
183
+ readonly invalid_first_contact: {
184
+ readonly category: "integrity";
185
+ readonly recovery: "contact_support";
186
+ };
179
187
  readonly invalid_loopback_url: {
180
188
  readonly category: "validation";
181
189
  readonly recovery: "fix_request";
@@ -438,6 +446,7 @@ export interface ConnectProblemDetailsByCode {
438
446
  "file_not_found": undefined;
439
447
  "file_source_mismatch": undefined;
440
448
  "file_upload_incomplete": undefined;
449
+ "first_contact_handler_required": undefined;
441
450
  "hosted_provider_unavailable": undefined;
442
451
  "insufficient_access": {
443
452
  "required_operations": Array<string>;
@@ -448,6 +457,7 @@ export interface ConnectProblemDetailsByCode {
448
457
  "invalid_callback": undefined;
449
458
  "invalid_device_authorization_response": undefined;
450
459
  "invalid_encrypted_response": undefined;
460
+ "invalid_first_contact": undefined;
451
461
  "invalid_loopback_url": undefined;
452
462
  "invalid_mutation_id": undefined;
453
463
  "invalid_operation_response": undefined;
@@ -730,6 +740,12 @@ export interface ConnectProblemByCode {
730
740
  recovery: "retry";
731
741
  details?: never;
732
742
  };
743
+ "first_contact_handler_required": ConnectProblemBase & {
744
+ code: "first_contact_handler_required";
745
+ category: "compatibility";
746
+ recovery: "upgrade_application";
747
+ details?: never;
748
+ };
733
749
  "hosted_provider_unavailable": ConnectProblemBase & {
734
750
  code: "hosted_provider_unavailable";
735
751
  category: "availability";
@@ -766,6 +782,12 @@ export interface ConnectProblemByCode {
766
782
  recovery: "contact_support";
767
783
  details?: never;
768
784
  };
785
+ "invalid_first_contact": ConnectProblemBase & {
786
+ code: "invalid_first_contact";
787
+ category: "integrity";
788
+ recovery: "contact_support";
789
+ details?: never;
790
+ };
769
791
  "invalid_loopback_url": ConnectProblemBase & {
770
792
  code: "invalid_loopback_url";
771
793
  category: "validation";
@@ -39,12 +39,14 @@ export const CONNECT_PROBLEM_CATALOG = {
39
39
  "file_not_found": { category: "conflict", recovery: "refresh" },
40
40
  "file_source_mismatch": { category: "conflict", recovery: "refresh" },
41
41
  "file_upload_incomplete": { category: "conflict", recovery: "retry" },
42
+ "first_contact_handler_required": { category: "compatibility", recovery: "upgrade_application" },
42
43
  "hosted_provider_unavailable": { category: "availability", recovery: "retry" },
43
44
  "insufficient_access": { category: "authorization", recovery: "reauthorize" },
44
45
  "invalid_application_manifest": { category: "validation", recovery: "fix_request" },
45
46
  "invalid_callback": { category: "validation", recovery: "fix_request" },
46
47
  "invalid_device_authorization_response": { category: "integrity", recovery: "contact_support" },
47
48
  "invalid_encrypted_response": { category: "integrity", recovery: "contact_support" },
49
+ "invalid_first_contact": { category: "integrity", recovery: "contact_support" },
48
50
  "invalid_loopback_url": { category: "validation", recovery: "fix_request" },
49
51
  "invalid_mutation_id": { category: "validation", recovery: "fix_request" },
50
52
  "invalid_operation_response": { category: "integrity", recovery: "contact_support" },
package/dist/index.d.ts CHANGED
@@ -1,17 +1,22 @@
1
1
  import type { ConnectProblem } from "./connect-problems.generated.js";
2
2
  import type { ApplicationFileRequirement, CollectionFileDescriptor, FileCapability } from "./files.js";
3
+ import type { CollectionOperation } from "./operations.js";
4
+ import type { ApplicationAuthorizationProof } from "./application-authorization.js";
3
5
  export * from "./connect-problems.generated.js";
4
6
  export * from "./files.js";
7
+ export * from "./operations.js";
8
+ export * from "./application-authorization.js";
5
9
  export declare const CONTROL_PROTOCOL_VERSION: 1;
6
10
  export declare const ENCRYPTED_RELAY_PROTOCOL_VERSION: 1;
11
+ export declare const FIRST_CONTACT_PROTOCOL_VERSION: 1;
7
12
  export declare const LOOPBACK_PROTOCOL_VERSION: 1;
8
13
  export declare const DEFAULT_LOOPBACK_PORT: 28485;
9
14
  export declare const RELAY_ENCRYPTION_SUITE: "P256-HKDF-SHA256-AES256GCM";
10
15
  export declare const SYNC_PROTOCOL_VERSION: 1;
11
16
  export declare const CONTRACT_SETUP_CAPABILITY: "contract-setup-v1";
12
17
  export declare const FILE_RELAY_CAPABILITY: "file-relay-v1";
13
- export declare const RELAY_REQUIRED_CAPABILITIES: readonly ["authorization-activation", "encrypted-relay", "policy-ack"];
14
- export declare const RELAY_CAPABILITIES: readonly ["authorization-activation", "encrypted-relay", "policy-ack", "contract-setup-v1", "file-relay-v1"];
18
+ export declare const RELAY_REQUIRED_CAPABILITIES: readonly ["application-trust-v1", "authorization-activation", "encrypted-relay", "policy-ack"];
19
+ export declare const RELAY_CAPABILITIES: readonly ["application-trust-v1", "authorization-activation", "encrypted-relay", "policy-ack", "contract-setup-v1", "file-relay-v1"];
15
20
  export declare const AUTHORITY_PROOF_VERSION: 1;
16
21
  export declare const AUTHORITY_PROOF_ALGORITHM: "P256-SHA256";
17
22
  export declare const AUTHORITY_PROOF_DOMAIN: "mdbase-authority-request-proof-v1";
@@ -37,7 +42,6 @@ export declare const CONNECT_SCHEMA_IDS: {
37
42
  readonly files: "https://mdbase.dev/connect/schemas/files.v1.json";
38
43
  readonly sync: "https://mdbase.dev/connect/schemas/sync.v1.json";
39
44
  };
40
- export type CollectionOperation = "describe" | "changes" | "read" | "query" | "list_views" | "execute_view" | "read_view_source" | "create_view_source" | "update_view_source" | "delete_view_source" | "validate" | "create" | "update" | "delete" | "rename" | "read_type" | "create_type" | "update_type" | "install_type_pack" | "list_timers" | "put_timer" | "cancel_timer" | "reconcile_timers" | "sync";
41
45
  export interface NotificationCriterion {
42
46
  /** Stable, manifest-owned identifier selected by an installation. */
43
47
  id: string;
@@ -275,7 +279,11 @@ export interface GrantPolicy {
275
279
  created_at: string;
276
280
  encryption?: GrantEncryption;
277
281
  file_capability?: FileCapability;
282
+ first_contact: FirstContactBinding;
283
+ application_authorization: ApplicationAuthorizationProof;
278
284
  }
285
+ /** Presentation-only grant shape used outside a local authorization boundary. */
286
+ export type GrantSummary = Omit<GrantPolicy, "first_contact" | "application_authorization">;
279
287
  export interface GrantEncryption {
280
288
  protocol_version: 1;
281
289
  suite: typeof RELAY_ENCRYPTION_SUITE;
@@ -286,6 +294,15 @@ export interface GrantEncryption {
286
294
  application_agreement_public_key: string;
287
295
  connector_agreement_public_key: string;
288
296
  }
297
+ export interface FirstContactBinding {
298
+ protocol_version: typeof FIRST_CONTACT_PROTOCOL_VERSION;
299
+ application_id: string;
300
+ application_installation_id: string;
301
+ application_agreement_public_key: string;
302
+ application_signing_public_key: string;
303
+ connector_id: string;
304
+ connector_agreement_public_key: string;
305
+ }
289
306
  export interface EncryptedRelayEnvelope {
290
307
  protocol_version: 1;
291
308
  suite: typeof RELAY_ENCRYPTION_SUITE;
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  export * from "./connect-problems.generated.js";
2
2
  export * from "./files.js";
3
+ export * from "./operations.js";
4
+ export * from "./application-authorization.js";
3
5
  export const CONTROL_PROTOCOL_VERSION = 1;
4
6
  export const ENCRYPTED_RELAY_PROTOCOL_VERSION = 1;
7
+ export const FIRST_CONTACT_PROTOCOL_VERSION = 1;
5
8
  export const LOOPBACK_PROTOCOL_VERSION = 1;
6
9
  export const DEFAULT_LOOPBACK_PORT = 28_485;
7
10
  export const RELAY_ENCRYPTION_SUITE = "P256-HKDF-SHA256-AES256GCM";
@@ -9,6 +12,7 @@ export const SYNC_PROTOCOL_VERSION = 1;
9
12
  export const CONTRACT_SETUP_CAPABILITY = "contract-setup-v1";
10
13
  export const FILE_RELAY_CAPABILITY = "file-relay-v1";
11
14
  export const RELAY_REQUIRED_CAPABILITIES = [
15
+ "application-trust-v1",
12
16
  "authorization-activation",
13
17
  "encrypted-relay",
14
18
  "policy-ack"
@@ -0,0 +1 @@
1
+ export type CollectionOperation = "describe" | "changes" | "read" | "query" | "list_views" | "execute_view" | "read_view_source" | "create_view_source" | "update_view_source" | "delete_view_source" | "validate" | "create" | "update" | "delete" | "rename" | "read_type" | "create_type" | "update_type" | "install_type_pack" | "list_timers" | "put_timer" | "cancel_timer" | "reconcile_timers" | "sync";
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdbase-dev/connect-protocol",
3
- "version": "0.1.0-beta.22",
3
+ "version": "0.1.0-beta.24",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1666,6 +1666,46 @@
1666
1666
  }
1667
1667
  }
1668
1668
  },
1669
+ {
1670
+ "type": "object",
1671
+ "additionalProperties": false,
1672
+ "required": [
1673
+ "problem_version",
1674
+ "code",
1675
+ "category",
1676
+ "recovery",
1677
+ "message"
1678
+ ],
1679
+ "properties": {
1680
+ "problem_version": {
1681
+ "const": 1
1682
+ },
1683
+ "code": {
1684
+ "const": "first_contact_handler_required"
1685
+ },
1686
+ "category": {
1687
+ "const": "compatibility"
1688
+ },
1689
+ "recovery": {
1690
+ "const": "upgrade_application"
1691
+ },
1692
+ "message": {
1693
+ "type": "string",
1694
+ "minLength": 1
1695
+ },
1696
+ "operation_outcome": {
1697
+ "enum": [
1698
+ "not_sent",
1699
+ "rejected",
1700
+ "unknown"
1701
+ ]
1702
+ },
1703
+ "trace_id": {
1704
+ "type": "string",
1705
+ "minLength": 1
1706
+ }
1707
+ }
1708
+ },
1669
1709
  {
1670
1710
  "type": "object",
1671
1711
  "additionalProperties": false,
@@ -1936,6 +1976,46 @@
1936
1976
  }
1937
1977
  }
1938
1978
  },
1979
+ {
1980
+ "type": "object",
1981
+ "additionalProperties": false,
1982
+ "required": [
1983
+ "problem_version",
1984
+ "code",
1985
+ "category",
1986
+ "recovery",
1987
+ "message"
1988
+ ],
1989
+ "properties": {
1990
+ "problem_version": {
1991
+ "const": 1
1992
+ },
1993
+ "code": {
1994
+ "const": "invalid_first_contact"
1995
+ },
1996
+ "category": {
1997
+ "const": "integrity"
1998
+ },
1999
+ "recovery": {
2000
+ "const": "contact_support"
2001
+ },
2002
+ "message": {
2003
+ "type": "string",
2004
+ "minLength": 1
2005
+ },
2006
+ "operation_outcome": {
2007
+ "enum": [
2008
+ "not_sent",
2009
+ "rejected",
2010
+ "unknown"
2011
+ ]
2012
+ },
2013
+ "trace_id": {
2014
+ "type": "string",
2015
+ "minLength": 1
2016
+ }
2017
+ }
2018
+ },
1939
2019
  {
1940
2020
  "type": "object",
1941
2021
  "additionalProperties": false,
@@ -151,6 +151,7 @@
151
151
  "file_not_found": { "category": "conflict", "recovery": "refresh" },
152
152
  "file_source_mismatch": { "category": "conflict", "recovery": "refresh" },
153
153
  "file_upload_incomplete": { "category": "conflict", "recovery": "retry" },
154
+ "first_contact_handler_required": { "category": "compatibility", "recovery": "upgrade_application" },
154
155
  "hosted_provider_unavailable": { "category": "availability", "recovery": "retry" },
155
156
  "insufficient_access": {
156
157
  "category": "authorization",
@@ -168,6 +169,7 @@
168
169
  "invalid_callback": { "category": "validation", "recovery": "fix_request" },
169
170
  "invalid_device_authorization_response": { "category": "integrity", "recovery": "contact_support" },
170
171
  "invalid_encrypted_response": { "category": "integrity", "recovery": "contact_support" },
172
+ "invalid_first_contact": { "category": "integrity", "recovery": "contact_support" },
171
173
  "invalid_loopback_url": { "category": "validation", "recovery": "fix_request" },
172
174
  "invalid_mutation_id": { "category": "validation", "recovery": "fix_request" },
173
175
  "invalid_operation_response": { "category": "integrity", "recovery": "contact_support" },
@@ -202,7 +202,7 @@
202
202
  "grantPolicy": {
203
203
  "type": "object",
204
204
  "additionalProperties": false,
205
- "required": ["id", "application_id", "collection_id", "operations", "scope", "application_name", "application_homepage", "collection_name", "created_at"],
205
+ "required": ["id", "application_id", "collection_id", "operations", "scope", "application_name", "application_distribution", "application_homepage", "application_origin", "collection_name", "created_at", "encryption", "first_contact", "application_authorization"],
206
206
  "properties": {
207
207
  "id": { "$ref": "#/$defs/uuid" },
208
208
  "application_id": { "$ref": "#/$defs/uuid" },
@@ -230,9 +230,70 @@
230
230
  "encryption": { "$ref": "#/$defs/grantEncryption" },
231
231
  "file_capability": {
232
232
  "$ref": "https://mdbase.dev/connect/schemas/files.v1.json#/$defs/fileCapability"
233
- }
233
+ },
234
+ "first_contact": { "$ref": "#/$defs/firstContactBinding" },
235
+ "application_authorization": { "$ref": "#/$defs/applicationAuthorizationProof" }
236
+ }
237
+ },
238
+ "firstContactBinding": {
239
+ "type": "object",
240
+ "additionalProperties": false,
241
+ "required": ["protocol_version", "application_id", "application_installation_id", "application_agreement_public_key", "application_signing_public_key", "connector_id", "connector_agreement_public_key"],
242
+ "properties": {
243
+ "protocol_version": { "const": 1 },
244
+ "application_id": { "$ref": "#/$defs/uuid" },
245
+ "application_installation_id": { "$ref": "#/$defs/uuid" },
246
+ "application_agreement_public_key": { "$ref": "#/$defs/p256PublicKey" },
247
+ "application_signing_public_key": { "$ref": "#/$defs/p256PublicKey" },
248
+ "connector_id": { "$ref": "#/$defs/uuid" },
249
+ "connector_agreement_public_key": { "$ref": "#/$defs/p256PublicKey" }
250
+ }
251
+ },
252
+ "applicationAuthorizationProof": {
253
+ "type": "object",
254
+ "additionalProperties": false,
255
+ "required": ["binding", "signature"],
256
+ "properties": {
257
+ "binding": { "$ref": "#/$defs/applicationAuthorizationBinding" },
258
+ "signature": { "type": "string", "pattern": "^[A-Za-z0-9_-]{86}$" }
234
259
  }
235
260
  },
261
+ "applicationAuthorizationBinding": {
262
+ "type": "object",
263
+ "additionalProperties": false,
264
+ "required": ["protocol_version", "authorization_id", "application_id", "application_manifest_digest", "application_installation_id", "installation_agreement_public_key", "installation_signing_public_key", "grant_agreement_public_key", "grant_signing_public_key", "flow", "authorization_nonce", "issued_at", "expires_at", "code_challenge", "requested_operations"],
265
+ "properties": {
266
+ "protocol_version": { "const": 1 },
267
+ "authorization_id": { "$ref": "#/$defs/uuid" },
268
+ "application_id": { "$ref": "#/$defs/uuid" },
269
+ "application_manifest_digest": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
270
+ "application_installation_id": { "$ref": "#/$defs/uuid" },
271
+ "installation_agreement_public_key": { "$ref": "#/$defs/p256PublicKey" },
272
+ "installation_signing_public_key": { "$ref": "#/$defs/p256PublicKey" },
273
+ "grant_agreement_public_key": { "$ref": "#/$defs/p256PublicKey" },
274
+ "grant_signing_public_key": { "$ref": "#/$defs/p256PublicKey" },
275
+ "flow": { "enum": ["authorization_code", "device_code"] },
276
+ "authorization_nonce": { "type": "string", "pattern": "^[A-Za-z0-9_-]{43}$" },
277
+ "issued_at": { "type": "string", "format": "date-time" },
278
+ "expires_at": { "type": "string", "format": "date-time" },
279
+ "redirect_uri": { "type": "string", "format": "uri" },
280
+ "state": { "type": "string", "maxLength": 500 },
281
+ "code_challenge": { "type": "string", "pattern": "^[A-Za-z0-9_-]{43}$" },
282
+ "requested_operations": {
283
+ "type": "array",
284
+ "uniqueItems": true,
285
+ "items": { "$ref": "#/$defs/operation" }
286
+ },
287
+ "requested_files": {
288
+ "$ref": "https://mdbase.dev/connect/schemas/files.v1.json#/$defs/applicationFileRequirement"
289
+ },
290
+ "collection_id": { "$ref": "#/$defs/uuid" }
291
+ }
292
+ },
293
+ "p256PublicKey": {
294
+ "type": "string",
295
+ "pattern": "^[A-Za-z0-9_-]{87}$"
296
+ },
236
297
  "authorizationCollectionOffer": {
237
298
  "type": "object",
238
299
  "additionalProperties": false,