stellarium-ts 0.12.0 → 0.14.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.
- package/dist/client/operatorClient.d.ts +10 -4
- package/dist/client/operatorClient.d.ts.map +1 -1
- package/dist/client/operatorClient.js +51 -7
- package/dist/crypto/utils.d.ts +9 -0
- package/dist/crypto/utils.d.ts.map +1 -1
- package/dist/crypto/utils.js +30 -1
- package/dist/db/db.d.ts +5 -4
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +93 -12
- package/dist/grant/grant.d.ts +132 -0
- package/dist/grant/grant.d.ts.map +1 -0
- package/dist/grant/grant.js +158 -0
- package/dist/grant/repository.d.ts +57 -0
- package/dist/grant/repository.d.ts.map +1 -0
- package/dist/grant/repository.js +105 -0
- package/dist/grant/service.d.ts +141 -0
- package/dist/grant/service.d.ts.map +1 -0
- package/dist/grant/service.js +431 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/peer/repository.d.ts +33 -2
- package/dist/peer/repository.d.ts.map +1 -1
- package/dist/peer/repository.js +43 -21
- package/dist/resource/invoker.d.ts +21 -9
- package/dist/resource/invoker.d.ts.map +1 -1
- package/dist/resource/invoker.js +69 -16
- package/dist/resource/repository.d.ts +56 -9
- package/dist/resource/repository.d.ts.map +1 -1
- package/dist/resource/repository.js +140 -68
- package/dist/routes.d.ts +8 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +310 -49
- package/dist/server.d.ts +62 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +119 -18
- package/dist/service.d.ts +24 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +9 -6
- package/dist/startup.d.ts +14 -0
- package/dist/startup.d.ts.map +1 -1
- package/dist/startup.js +90 -76
- package/dist/tests/client/operatorClient.test.d.ts +2 -0
- package/dist/tests/client/operatorClient.test.d.ts.map +1 -0
- package/dist/tests/client/operatorClient.test.js +96 -0
- package/dist/tests/db/db.test.d.ts +2 -0
- package/dist/tests/db/db.test.d.ts.map +1 -0
- package/dist/tests/db/db.test.js +64 -0
- package/dist/tests/grant/grant.test.d.ts +2 -0
- package/dist/tests/grant/grant.test.d.ts.map +1 -0
- package/dist/tests/grant/grant.test.js +133 -0
- package/dist/tests/grant/service.test.d.ts +2 -0
- package/dist/tests/grant/service.test.d.ts.map +1 -0
- package/dist/tests/grant/service.test.js +415 -0
- package/dist/tests/helpers.d.ts +13 -0
- package/dist/tests/helpers.d.ts.map +1 -0
- package/dist/tests/helpers.js +49 -0
- package/dist/tests/integration/network.test.d.ts +2 -0
- package/dist/tests/integration/network.test.d.ts.map +1 -0
- package/dist/tests/integration/network.test.js +188 -0
- package/dist/tests/peer/repository.test.d.ts +2 -0
- package/dist/tests/peer/repository.test.d.ts.map +1 -0
- package/dist/tests/peer/repository.test.js +50 -0
- package/dist/tests/resource/invoker.test.js +132 -5
- package/dist/tests/resource/repository.test.d.ts +2 -0
- package/dist/tests/resource/repository.test.d.ts.map +1 -0
- package/dist/tests/resource/repository.test.js +182 -0
- package/dist/tests/routes.test.d.ts +2 -0
- package/dist/tests/routes.test.d.ts.map +1 -0
- package/dist/tests/routes.test.js +565 -0
- package/dist/tests/server.test.d.ts +2 -0
- package/dist/tests/server.test.d.ts.map +1 -0
- package/dist/tests/server.test.js +37 -0
- package/dist/tests/startup.test.d.ts +2 -0
- package/dist/tests/startup.test.d.ts.map +1 -0
- package/dist/tests/startup.test.js +75 -0
- package/package.json +2 -1
|
@@ -6,15 +6,21 @@ export interface PaymentSubmission {
|
|
|
6
6
|
recipientAddress: string;
|
|
7
7
|
signature: string;
|
|
8
8
|
expiresAt: string;
|
|
9
|
+
invocationHash: string;
|
|
9
10
|
publicKey: string;
|
|
10
11
|
};
|
|
11
|
-
resourcePath: string;
|
|
12
|
-
resourceName: string;
|
|
13
|
-
parameters: unknown[];
|
|
14
12
|
}
|
|
13
|
+
export declare const SETTLE_PAYMENT_MAX_RETRIES = 5;
|
|
14
|
+
export declare const SETTLE_PAYMENT_BASE_DELAY_MS = 1000;
|
|
15
|
+
/** Per attempt. A timed-out attempt is retried like a network error. */
|
|
16
|
+
export declare const SETTLE_PAYMENT_TIMEOUT_MS = 10000;
|
|
15
17
|
export declare class OperatorClient {
|
|
16
18
|
private readonly url;
|
|
17
|
-
constructor(url: string);
|
|
19
|
+
constructor(url: string | undefined);
|
|
20
|
+
/**
|
|
21
|
+
* Retries `RETRYABLE_STATUSES` and network errors up to `SETTLE_PAYMENT_MAX_RETRIES` times.
|
|
22
|
+
* Throws the last network error if the operator could not be reached, or if no operator URL is configured.
|
|
23
|
+
*/
|
|
18
24
|
settlePayment(payment: PaymentSubmission): Promise<void>;
|
|
19
25
|
}
|
|
20
26
|
//# sourceMappingURL=operatorClient.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operatorClient.d.ts","sourceRoot":"","sources":["../../src/client/operatorClient.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAC9B,OAAO,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC;QACf,gBAAgB,EAAE,MAAM,CAAC;QACzB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;KACrB,CAAA;
|
|
1
|
+
{"version":3,"file":"operatorClient.d.ts","sourceRoot":"","sources":["../../src/client/operatorClient.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAC9B,OAAO,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC;QACf,gBAAgB,EAAE,MAAM,CAAC;QACzB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,CAAC;QACvB,SAAS,EAAE,MAAM,CAAC;KACrB,CAAA;CACJ;AAED,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAC5C,eAAO,MAAM,4BAA4B,OAAO,CAAC;AACjD,wEAAwE;AACxE,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAmBhD,qBAAa,cAAc;IACX,OAAO,CAAC,QAAQ,CAAC,GAAG;IAAhC,YAA6B,GAAG,EAAE,MAAM,GAAG,SAAS,EAAI;IAExD;;;OAGG;IACG,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+B7D;CACJ"}
|
|
@@ -1,16 +1,60 @@
|
|
|
1
|
+
export const SETTLE_PAYMENT_MAX_RETRIES = 5;
|
|
2
|
+
export const SETTLE_PAYMENT_BASE_DELAY_MS = 1000;
|
|
3
|
+
/** Per attempt. A timed-out attempt is retried like a network error. */
|
|
4
|
+
export const SETTLE_PAYMENT_TIMEOUT_MS = 10_000;
|
|
5
|
+
/**
|
|
6
|
+
* Transient failures. Resending is safe even if an earlier attempt reached the operator: it stores a
|
|
7
|
+
* unique receipt per (requestId, invocationId) before paying, so a repeat is rejected with 409.
|
|
8
|
+
* Rejections (400, 401, 404, 409, 422) are not retried because they won't change.
|
|
9
|
+
*/
|
|
10
|
+
const RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
|
|
11
|
+
/**
|
|
12
|
+
* Delay before retry `attempt` (0-based): exponential backoff (1s, 2s, 4s, 8s, 16s),
|
|
13
|
+
* or the operator's `Retry-After` seconds when that is longer.
|
|
14
|
+
*/
|
|
15
|
+
function retryDelayMs(response, attempt) {
|
|
16
|
+
const backoff = SETTLE_PAYMENT_BASE_DELAY_MS * 2 ** attempt;
|
|
17
|
+
const retryAfterSeconds = Number(response?.headers.get('retry-after') ?? NaN);
|
|
18
|
+
return Number.isFinite(retryAfterSeconds) ? Math.max(backoff, retryAfterSeconds * 1000) : backoff;
|
|
19
|
+
}
|
|
1
20
|
export class OperatorClient {
|
|
2
21
|
url;
|
|
3
22
|
constructor(url) {
|
|
4
23
|
this.url = url;
|
|
5
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Retries `RETRYABLE_STATUSES` and network errors up to `SETTLE_PAYMENT_MAX_RETRIES` times.
|
|
27
|
+
* Throws the last network error if the operator could not be reached, or if no operator URL is configured.
|
|
28
|
+
*/
|
|
6
29
|
async settlePayment(payment) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
30
|
+
if (!this.url) {
|
|
31
|
+
throw new Error('Cannot settle payment: no operator URL is configured');
|
|
32
|
+
}
|
|
33
|
+
const body = JSON.stringify(payment);
|
|
34
|
+
let response;
|
|
35
|
+
for (let attempt = 0;; attempt++) {
|
|
36
|
+
let retryable;
|
|
37
|
+
try {
|
|
38
|
+
response = await fetch(`${this.url}/payment`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
body,
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
},
|
|
44
|
+
signal: AbortSignal.timeout(SETTLE_PAYMENT_TIMEOUT_MS),
|
|
45
|
+
});
|
|
46
|
+
if (!RETRYABLE_STATUSES.has(response.status) || attempt >= SETTLE_PAYMENT_MAX_RETRIES) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
retryable = response;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (attempt >= SETTLE_PAYMENT_MAX_RETRIES) {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
await new Promise(resolve => setTimeout(resolve, retryDelayMs(retryable, attempt)));
|
|
57
|
+
}
|
|
14
58
|
if (!response.ok) {
|
|
15
59
|
console.error(`Failed to settle payment: ${response.statusText} body: ${await response.text()}`);
|
|
16
60
|
}
|
package/dist/crypto/utils.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
export declare function signData(data: Object, privateKey: string): string;
|
|
2
|
+
export declare function hashData(data: Object): string;
|
|
2
3
|
export declare function verifyData(data: Object, publicKey: string, signature: string): boolean;
|
|
4
|
+
/** Longest lifetime accepted for a signed peer or resource broadcast, so a captured one can't be replayed indefinitely. */
|
|
5
|
+
export declare const MAX_SIGNATURE_LIFETIME_MS: number;
|
|
6
|
+
/** True when `expiry` is a valid date in the future, no more than `maxLifetimeMs` away. */
|
|
7
|
+
export declare function isFreshExpiry(expiry: string, maxLifetimeMs?: number): boolean;
|
|
8
|
+
/** True when `publicKey` is an Ed25519 SPKI DER key in base64url, the form nodes sign with. */
|
|
9
|
+
export declare function isValidPublicKey(publicKey: string): boolean;
|
|
10
|
+
/** verifyData that treats a malformed key or signature as an invalid signature instead of throwing. */
|
|
11
|
+
export declare function safeVerifyData(data: Object, publicKey: string, signature: string): boolean;
|
|
3
12
|
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/crypto/utils.ts"],"names":[],"mappings":"AASA,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAQjE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAQtF"}
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/crypto/utils.ts"],"names":[],"mappings":"AASA,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAQjE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAQtF;AAED,2HAA2H;AAC3H,eAAO,MAAM,yBAAyB,QAAiB,CAAC;AAExD,2FAA2F;AAC3F,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,GAAE,MAAkC,GAAG,OAAO,CAIxG;AAED,+FAA+F;AAC/F,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAM3D;AAED,uGAAuG;AACvG,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAM1F"}
|
package/dist/crypto/utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createPrivateKey, createPublicKey, sign, verify, } from "node:crypto";
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, sign, verify, } from "node:crypto";
|
|
2
2
|
export function signData(data, privateKey) {
|
|
3
3
|
const key = createPrivateKey({
|
|
4
4
|
key: Buffer.from(privateKey, "base64url"),
|
|
@@ -8,6 +8,9 @@ export function signData(data, privateKey) {
|
|
|
8
8
|
const bytes = Buffer.from(JSON.stringify(data));
|
|
9
9
|
return sign(null, bytes, key).toString('base64url');
|
|
10
10
|
}
|
|
11
|
+
export function hashData(data) {
|
|
12
|
+
return createHash('sha256').update(JSON.stringify(data)).digest('base64url');
|
|
13
|
+
}
|
|
11
14
|
export function verifyData(data, publicKey, signature) {
|
|
12
15
|
const key = createPublicKey({
|
|
13
16
|
key: Buffer.from(publicKey, "base64url"),
|
|
@@ -17,3 +20,29 @@ export function verifyData(data, publicKey, signature) {
|
|
|
17
20
|
const bytes = Buffer.from(JSON.stringify(data));
|
|
18
21
|
return verify(null, bytes, key, Buffer.from(signature, 'base64url'));
|
|
19
22
|
}
|
|
23
|
+
/** Longest lifetime accepted for a signed peer or resource broadcast, so a captured one can't be replayed indefinitely. */
|
|
24
|
+
export const MAX_SIGNATURE_LIFETIME_MS = 1000 * 60 * 60;
|
|
25
|
+
/** True when `expiry` is a valid date in the future, no more than `maxLifetimeMs` away. */
|
|
26
|
+
export function isFreshExpiry(expiry, maxLifetimeMs = MAX_SIGNATURE_LIFETIME_MS) {
|
|
27
|
+
const expiresAt = Date.parse(expiry);
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
return Number.isFinite(expiresAt) && expiresAt > now && expiresAt <= now + maxLifetimeMs;
|
|
30
|
+
}
|
|
31
|
+
/** True when `publicKey` is an Ed25519 SPKI DER key in base64url, the form nodes sign with. */
|
|
32
|
+
export function isValidPublicKey(publicKey) {
|
|
33
|
+
try {
|
|
34
|
+
return createPublicKey({ key: Buffer.from(publicKey, "base64url"), format: "der", type: "spki" }).asymmetricKeyType === "ed25519";
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** verifyData that treats a malformed key or signature as an invalid signature instead of throwing. */
|
|
41
|
+
export function safeVerifyData(data, publicKey, signature) {
|
|
42
|
+
try {
|
|
43
|
+
return verifyData(data, publicKey, signature);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/db/db.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
declare const
|
|
3
|
-
|
|
4
|
-
export
|
|
1
|
+
import { type Database } from 'sqlite';
|
|
2
|
+
export declare const DEFAULT_DATABASE_PATH = "./requestscript.db";
|
|
3
|
+
/** Opens (or creates) the node database and brings its schema up to date. Pass ':memory:' for tests. */
|
|
4
|
+
export declare function openDatabase(filename?: string): Promise<Database>;
|
|
5
|
+
export declare function createTables(db: Database): Promise<void>;
|
|
5
6
|
//# sourceMappingURL=db.d.ts.map
|
package/dist/db/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db/db.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db/db.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,KAAK,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAE7C,eAAO,MAAM,qBAAqB,uBAAuB,CAAC;AAU1D,wGAAwG;AACxG,wBAAsB,YAAY,CAAC,QAAQ,GAAE,MAA8B,GAAG,OAAO,CAAC,QAAQ,CAAC,CAI9F;AAED,wBAAsB,YAAY,CAAC,EAAE,EAAE,QAAQ,iBA0I9C"}
|
package/dist/db/db.js
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
import sqlite3 from 'sqlite3';
|
|
2
2
|
import { open } from 'sqlite';
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
export const DEFAULT_DATABASE_PATH = './requestscript.db';
|
|
4
|
+
/**
|
|
5
|
+
* Bumped whenever the schema changes in a way `CREATE TABLE IF NOT EXISTS` can't apply.
|
|
6
|
+
* Version 1 dropped `UNIQUE (base_url)` from `resource` and added uniqueness to the function tables.
|
|
7
|
+
* Version 2 added `resource_function.restricted`.
|
|
8
|
+
* Version 3 added `resource.website` and the `description` columns of the resource tables.
|
|
9
|
+
*/
|
|
10
|
+
const SCHEMA_VERSION = 3;
|
|
11
|
+
/** Opens (or creates) the node database and brings its schema up to date. Pass ':memory:' for tests. */
|
|
12
|
+
export async function openDatabase(filename = DEFAULT_DATABASE_PATH) {
|
|
13
|
+
const db = await open({ filename, driver: sqlite3.Database });
|
|
14
|
+
await createTables(db);
|
|
15
|
+
return db;
|
|
16
|
+
}
|
|
17
|
+
export async function createTables(db) {
|
|
18
|
+
const { user_version: version } = await db.get('PRAGMA user_version') ?? { user_version: 0 };
|
|
19
|
+
if (version < SCHEMA_VERSION) {
|
|
20
|
+
// Resource rows are a cache of what peers advertise, and are relearned on the next broadcast.
|
|
21
|
+
// SQLite can't drop a constraint in place, so rebuild them. Peers are kept.
|
|
22
|
+
await db.exec(`
|
|
23
|
+
DROP TABLE IF EXISTS resource_function_payment_manifest;
|
|
24
|
+
DROP TABLE IF EXISTS resource_function_parameter;
|
|
25
|
+
DROP TABLE IF EXISTS resource_function;
|
|
26
|
+
DROP TABLE IF EXISTS resource;
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
9
29
|
await db.exec(`
|
|
10
30
|
CREATE TABLE IF NOT EXISTS peer (
|
|
11
31
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -26,13 +46,14 @@ export async function createTables() {
|
|
|
26
46
|
path TEXT NOT NULL,
|
|
27
47
|
name TEXT NOT NULL,
|
|
28
48
|
base_url TEXT NOT NULL,
|
|
49
|
+
website TEXT,
|
|
50
|
+
description TEXT,
|
|
29
51
|
created_at TEXT NOT NULL,
|
|
30
52
|
updated_at TEXT NOT NULL,
|
|
31
53
|
|
|
32
54
|
FOREIGN KEY (peer_id) REFERENCES peer(id),
|
|
33
55
|
|
|
34
|
-
UNIQUE (path, name)
|
|
35
|
-
UNIQUE (base_url)
|
|
56
|
+
UNIQUE (path, name)
|
|
36
57
|
);
|
|
37
58
|
|
|
38
59
|
CREATE TABLE IF NOT EXISTS resource_function (
|
|
@@ -40,10 +61,14 @@ export async function createTables() {
|
|
|
40
61
|
resource_id INTEGER NOT NULL,
|
|
41
62
|
name TEXT NOT NULL,
|
|
42
63
|
return_type TEXT NOT NULL,
|
|
64
|
+
description TEXT,
|
|
65
|
+
restricted INTEGER NOT NULL DEFAULT 0,
|
|
43
66
|
created_at TEXT NOT NULL,
|
|
44
67
|
updated_at TEXT NOT NULL,
|
|
45
68
|
|
|
46
|
-
FOREIGN KEY (resource_id) REFERENCES resource(id)
|
|
69
|
+
FOREIGN KEY (resource_id) REFERENCES resource(id),
|
|
70
|
+
|
|
71
|
+
UNIQUE (resource_id, name)
|
|
47
72
|
);
|
|
48
73
|
|
|
49
74
|
CREATE TABLE IF NOT EXISTS resource_function_parameter (
|
|
@@ -51,10 +76,13 @@ export async function createTables() {
|
|
|
51
76
|
resource_function_id INTEGER NOT NULL,
|
|
52
77
|
name TEXT NOT NULL,
|
|
53
78
|
parameter_type TEXT NOT NULL,
|
|
79
|
+
description TEXT,
|
|
54
80
|
created_at TEXT NOT NULL,
|
|
55
81
|
updated_at TEXT NOT NULL,
|
|
56
82
|
|
|
57
|
-
FOREIGN KEY (resource_function_id) REFERENCES resource_function(id)
|
|
83
|
+
FOREIGN KEY (resource_function_id) REFERENCES resource_function(id),
|
|
84
|
+
|
|
85
|
+
UNIQUE (resource_function_id, name)
|
|
58
86
|
);
|
|
59
87
|
|
|
60
88
|
CREATE TABLE IF NOT EXISTS resource_function_payment_manifest (
|
|
@@ -65,8 +93,61 @@ export async function createTables() {
|
|
|
65
93
|
created_at TEXT NOT NULL,
|
|
66
94
|
updated_at TEXT NOT NULL,
|
|
67
95
|
|
|
68
|
-
FOREIGN KEY (resource_function_id) REFERENCES resource_function(id)
|
|
96
|
+
FOREIGN KEY (resource_function_id) REFERENCES resource_function(id),
|
|
97
|
+
|
|
98
|
+
UNIQUE (resource_function_id)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
-- Grants this node signed, kept so their audience can renew them.
|
|
102
|
+
-- A delegated grant's parent_grant_issuer and parent_grant_id name the held grant it narrows.
|
|
103
|
+
CREATE TABLE IF NOT EXISTS issued_grant (
|
|
104
|
+
id TEXT PRIMARY KEY,
|
|
105
|
+
audience TEXT NOT NULL,
|
|
106
|
+
capabilities TEXT NOT NULL,
|
|
107
|
+
parent_grant_issuer TEXT,
|
|
108
|
+
parent_grant_id TEXT,
|
|
109
|
+
lifetime_ms INTEGER NOT NULL,
|
|
110
|
+
renewable_until TEXT,
|
|
111
|
+
created_at TEXT NOT NULL
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
-- Chains whose leaf grant names this node as its audience, keyed by the leaf's issuer and id.
|
|
115
|
+
-- Issuers choose their own ids, so an id alone doesn't identify a grant.
|
|
116
|
+
CREATE TABLE IF NOT EXISTS held_grant (
|
|
117
|
+
issuer TEXT NOT NULL,
|
|
118
|
+
id TEXT NOT NULL,
|
|
119
|
+
root_issuer_base_url TEXT NOT NULL,
|
|
120
|
+
chain TEXT NOT NULL,
|
|
121
|
+
issued_at TEXT NOT NULL,
|
|
122
|
+
expires_at TEXT NOT NULL,
|
|
123
|
+
updated_at TEXT NOT NULL,
|
|
124
|
+
|
|
125
|
+
PRIMARY KEY (issuer, id)
|
|
126
|
+
);
|
|
127
|
+
CREATE INDEX IF NOT EXISTS held_grant_root ON held_grant (root_issuer_base_url, expires_at);
|
|
128
|
+
|
|
129
|
+
-- Keyed like held grants, so revoking one issuer's grant leaves another's with the same id alone.
|
|
130
|
+
CREATE TABLE IF NOT EXISTS revoked_grant (
|
|
131
|
+
issuer TEXT NOT NULL,
|
|
132
|
+
grant_id TEXT NOT NULL,
|
|
133
|
+
revoked_at TEXT NOT NULL,
|
|
134
|
+
|
|
135
|
+
PRIMARY KEY (issuer, grant_id)
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
-- publicKey:nonce of accepted authorizations and grant requests, until they expire.
|
|
139
|
+
CREATE TABLE IF NOT EXISTS seen_nonce (
|
|
140
|
+
key TEXT PRIMARY KEY,
|
|
141
|
+
expires_at INTEGER NOT NULL
|
|
142
|
+
);
|
|
143
|
+
CREATE INDEX IF NOT EXISTS seen_nonce_expires_at ON seen_nonce (expires_at);
|
|
144
|
+
|
|
145
|
+
-- Keys an admin blocked from calling this node or getting grants from it.
|
|
146
|
+
CREATE TABLE IF NOT EXISTS blocked_caller (
|
|
147
|
+
public_key TEXT PRIMARY KEY,
|
|
148
|
+
reason TEXT,
|
|
149
|
+
blocked_at TEXT NOT NULL
|
|
69
150
|
);
|
|
70
151
|
`);
|
|
152
|
+
await db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
71
153
|
}
|
|
72
|
-
export default db;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { ResourceFunctionCallParameter } from "requestscript";
|
|
2
|
+
/**
|
|
3
|
+
* What a grant lets its audience call. `resourcePath` is exact, a segment prefix such as
|
|
4
|
+
* `com.example.*`, or `*` for any path. An omitted `resourceName` or `functionName` matches any.
|
|
5
|
+
* Each constraint requires the parameter of that name to equal the value (compared as JSON).
|
|
6
|
+
*/
|
|
7
|
+
export interface Capability {
|
|
8
|
+
resourcePath: string;
|
|
9
|
+
resourceName?: string;
|
|
10
|
+
functionName?: string;
|
|
11
|
+
constraints?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A signed statement that `issuer` lets `audience` call `capabilities` until `expiresAt`.
|
|
15
|
+
* A root grant (`parent: null`) is issued by the provider. A delegated grant is issued by the
|
|
16
|
+
* previous grant's audience, may only narrow it, and names it by `grantHash`.
|
|
17
|
+
*/
|
|
18
|
+
export interface Grant {
|
|
19
|
+
/** Kept across renewals, so revoking an id revokes every renewal of it. */
|
|
20
|
+
id: string;
|
|
21
|
+
/** Ed25519 SPKI DER, base64url. */
|
|
22
|
+
issuer: string;
|
|
23
|
+
/** Where the audience renews this grant. */
|
|
24
|
+
issuerBaseUrl: string;
|
|
25
|
+
audience: string;
|
|
26
|
+
capabilities: Capability[];
|
|
27
|
+
issuedAt: string;
|
|
28
|
+
expiresAt: string;
|
|
29
|
+
parent: string | null;
|
|
30
|
+
signature: string;
|
|
31
|
+
}
|
|
32
|
+
/** Sent with a call to a restricted function. Signed by the leaf grant's audience. */
|
|
33
|
+
export interface InvocationAuthorization {
|
|
34
|
+
publicKey: string;
|
|
35
|
+
/** Root first. */
|
|
36
|
+
grants: Grant[];
|
|
37
|
+
nonce: string;
|
|
38
|
+
expiresAt: string;
|
|
39
|
+
signature: string;
|
|
40
|
+
}
|
|
41
|
+
/** Asks a grant's issuer for a fresh copy of it. Signed by the grant's audience. */
|
|
42
|
+
export interface GrantRenewalRequest {
|
|
43
|
+
grantId: string;
|
|
44
|
+
publicKey: string;
|
|
45
|
+
nonce: string;
|
|
46
|
+
expiresAt: string;
|
|
47
|
+
signature: string;
|
|
48
|
+
}
|
|
49
|
+
/** Asks a node for a new root grant. Signed by the key that would become its audience. */
|
|
50
|
+
export interface GrantRequest {
|
|
51
|
+
publicKey: string;
|
|
52
|
+
capabilities: Capability[];
|
|
53
|
+
/** Anything the node's approval hook needs, such as an invite code. */
|
|
54
|
+
details?: Record<string, unknown> | null;
|
|
55
|
+
nonce: string;
|
|
56
|
+
expiresAt: string;
|
|
57
|
+
signature: string;
|
|
58
|
+
}
|
|
59
|
+
/** Longest lifetime of one grant. Renewal extends access past it. */
|
|
60
|
+
export declare const MAX_GRANT_LIFETIME_MS: number;
|
|
61
|
+
/** Longest lifetime of an invocation authorization or a renewal request. */
|
|
62
|
+
export declare const MAX_AUTHORIZATION_LIFETIME_MS: number;
|
|
63
|
+
/** Longest delegation chain, root included. */
|
|
64
|
+
export declare const MAX_GRANT_CHAIN_LENGTH = 4;
|
|
65
|
+
/**
|
|
66
|
+
* How far ahead of the verifier's clock a signer's clock may run. Signers use the full lifetimes
|
|
67
|
+
* above, so verifiers accept expiries up to this much past them.
|
|
68
|
+
*/
|
|
69
|
+
export declare const CLOCK_SKEW_TOLERANCE_MS: number;
|
|
70
|
+
/** The object a grant's signature covers: every field except `signature`. Key order is part of the signed bytes. */
|
|
71
|
+
export declare function grantPayload(grant: Omit<Grant, "signature">): {
|
|
72
|
+
id: string;
|
|
73
|
+
issuer: string;
|
|
74
|
+
issuerBaseUrl: string;
|
|
75
|
+
audience: string;
|
|
76
|
+
capabilities: Capability[];
|
|
77
|
+
issuedAt: string;
|
|
78
|
+
expiresAt: string;
|
|
79
|
+
parent: string | null;
|
|
80
|
+
};
|
|
81
|
+
/** Names a grant as a delegated grant's `parent`. Independent of the key order the grant arrived in. */
|
|
82
|
+
export declare function grantHash(grant: Grant): string;
|
|
83
|
+
/** Key order is part of the signed bytes. */
|
|
84
|
+
export declare function invocationAuthorizationPayload(payload: {
|
|
85
|
+
invocationHash: string;
|
|
86
|
+
publicKey: string;
|
|
87
|
+
nonce: string;
|
|
88
|
+
expiresAt: string;
|
|
89
|
+
grantsHash: string;
|
|
90
|
+
}): {
|
|
91
|
+
invocationHash: string;
|
|
92
|
+
publicKey: string;
|
|
93
|
+
nonce: string;
|
|
94
|
+
expiresAt: string;
|
|
95
|
+
grantsHash: string;
|
|
96
|
+
};
|
|
97
|
+
export declare function grantsHash(grants: Grant[]): string;
|
|
98
|
+
/** Key order is part of the signed bytes. */
|
|
99
|
+
export declare function grantRenewalPayload(request: Omit<GrantRenewalRequest, "signature">): {
|
|
100
|
+
grantId: string;
|
|
101
|
+
publicKey: string;
|
|
102
|
+
nonce: string;
|
|
103
|
+
expiresAt: string;
|
|
104
|
+
};
|
|
105
|
+
/** Key order is part of the signed bytes. Omitted `details` are signed as null. */
|
|
106
|
+
export declare function grantRequestPayload(request: Omit<GrantRequest, "signature">): {
|
|
107
|
+
publicKey: string;
|
|
108
|
+
capabilities: Capability[];
|
|
109
|
+
details: Record<string, unknown> | null;
|
|
110
|
+
nonce: string;
|
|
111
|
+
expiresAt: string;
|
|
112
|
+
};
|
|
113
|
+
export declare function signGrant(grant: Omit<Grant, "signature">, privateKey: string): Grant;
|
|
114
|
+
export declare function isCapability(value: unknown): value is Capability;
|
|
115
|
+
export declare function isGrant(value: unknown): value is Grant;
|
|
116
|
+
export declare function isAuthorization(value: unknown): value is InvocationAuthorization;
|
|
117
|
+
export declare function capabilityMatches(capability: Capability, resourcePath: string, resourceName: string, functionName: string, parameters: ResourceFunctionCallParameter[]): boolean;
|
|
118
|
+
/** True when `child` allows nothing `parent` doesn't. A delegated grant may only narrow its parent. */
|
|
119
|
+
export declare function capabilityCovers(parent: Capability, child: Capability): boolean;
|
|
120
|
+
export type ChainResult = {
|
|
121
|
+
ok: true;
|
|
122
|
+
leaf: Grant;
|
|
123
|
+
} | {
|
|
124
|
+
ok: false;
|
|
125
|
+
reason: string;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Checks a chain's signatures, expiries and links, root first. Revocation is the caller's
|
|
129
|
+
* job, since it needs the verifier's own records.
|
|
130
|
+
*/
|
|
131
|
+
export declare function verifyChain(grants: unknown, rootKey: string): ChainResult;
|
|
132
|
+
//# sourceMappingURL=grant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grant.d.ts","sourceRoot":"","sources":["../../src/grant/grant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,eAAe,CAAC;AAGnE;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED;;;;GAIG;AACH,MAAM,WAAW,KAAK;IAClB,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,mCAAmC;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,UAAU,EAAE,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,sFAAsF;AACtF,MAAM,WAAW,uBAAuB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB;IAClB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,oFAAoF;AACpF,MAAM,WAAW,mBAAmB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,0FAA0F;AAC1F,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,UAAU,EAAE,CAAC;IAC3B,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,qEAAqE;AACrE,eAAO,MAAM,qBAAqB,QAA2B,CAAC;AAC9D,4EAA4E;AAC5E,eAAO,MAAM,6BAA6B,QAAgB,CAAC;AAC3D,+CAA+C;AAC/C,eAAO,MAAM,sBAAsB,IAAI,CAAC;AACxC;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAAY,CAAC;AAEjD,oHAAoH;AACpH,wBAAgB,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC;;;;;;;;;EAG3D;AAED,wGAAwG;AACxG,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAE9C;AAED,6CAA6C;AAC7C,wBAAgB,8BAA8B,CAAC,OAAO,EAAE;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE;;;;;;EAG1J;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAElD;AAED,6CAA6C;AAC7C,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,mBAAmB,EAAE,WAAW,CAAC;;;;;EAGlF;AAED,mFAAmF;AACnF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC;;;;;;EAG3E;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,KAAK,CAEpF;AAMD,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAMhE;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAOtD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,uBAAuB,CAGhF;AAiCD,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,6BAA6B,EAAE,GAAG,OAAO,CAehL;AAED,uGAAuG;AACvG,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAY/E;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,CAoCzE"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { hashData, isFreshExpiry, safeVerifyData, signData } from "../crypto/utils.js";
|
|
2
|
+
/** Longest lifetime of one grant. Renewal extends access past it. */
|
|
3
|
+
export const MAX_GRANT_LIFETIME_MS = 1000 * 60 * 60 * 24 * 30;
|
|
4
|
+
/** Longest lifetime of an invocation authorization or a renewal request. */
|
|
5
|
+
export const MAX_AUTHORIZATION_LIFETIME_MS = 1000 * 60 * 2;
|
|
6
|
+
/** Longest delegation chain, root included. */
|
|
7
|
+
export const MAX_GRANT_CHAIN_LENGTH = 4;
|
|
8
|
+
/**
|
|
9
|
+
* How far ahead of the verifier's clock a signer's clock may run. Signers use the full lifetimes
|
|
10
|
+
* above, so verifiers accept expiries up to this much past them.
|
|
11
|
+
*/
|
|
12
|
+
export const CLOCK_SKEW_TOLERANCE_MS = 1000 * 30;
|
|
13
|
+
/** The object a grant's signature covers: every field except `signature`. Key order is part of the signed bytes. */
|
|
14
|
+
export function grantPayload(grant) {
|
|
15
|
+
const { id, issuer, issuerBaseUrl, audience, capabilities, issuedAt, expiresAt, parent } = grant;
|
|
16
|
+
return { id, issuer, issuerBaseUrl, audience, capabilities, issuedAt, expiresAt, parent };
|
|
17
|
+
}
|
|
18
|
+
/** Names a grant as a delegated grant's `parent`. Independent of the key order the grant arrived in. */
|
|
19
|
+
export function grantHash(grant) {
|
|
20
|
+
return hashData({ ...grantPayload(grant), signature: grant.signature });
|
|
21
|
+
}
|
|
22
|
+
/** Key order is part of the signed bytes. */
|
|
23
|
+
export function invocationAuthorizationPayload(payload) {
|
|
24
|
+
const { invocationHash, publicKey, nonce, expiresAt, grantsHash } = payload;
|
|
25
|
+
return { invocationHash, publicKey, nonce, expiresAt, grantsHash };
|
|
26
|
+
}
|
|
27
|
+
export function grantsHash(grants) {
|
|
28
|
+
return hashData(grants.map(grantHash));
|
|
29
|
+
}
|
|
30
|
+
/** Key order is part of the signed bytes. */
|
|
31
|
+
export function grantRenewalPayload(request) {
|
|
32
|
+
const { grantId, publicKey, nonce, expiresAt } = request;
|
|
33
|
+
return { grantId, publicKey, nonce, expiresAt };
|
|
34
|
+
}
|
|
35
|
+
/** Key order is part of the signed bytes. Omitted `details` are signed as null. */
|
|
36
|
+
export function grantRequestPayload(request) {
|
|
37
|
+
const { publicKey, capabilities, details, nonce, expiresAt } = request;
|
|
38
|
+
return { publicKey, capabilities, details: details ?? null, nonce, expiresAt };
|
|
39
|
+
}
|
|
40
|
+
export function signGrant(grant, privateKey) {
|
|
41
|
+
return { ...grantPayload(grant), signature: signData(grantPayload(grant), privateKey) };
|
|
42
|
+
}
|
|
43
|
+
const isString = (value) => typeof value === "string";
|
|
44
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
46
|
+
export function isCapability(value) {
|
|
47
|
+
return isObject(value)
|
|
48
|
+
&& isString(value.resourcePath) && value.resourcePath.length > 0
|
|
49
|
+
&& (value.resourceName === undefined || isString(value.resourceName))
|
|
50
|
+
&& (value.functionName === undefined || isString(value.functionName))
|
|
51
|
+
&& (value.constraints === undefined || isObject(value.constraints));
|
|
52
|
+
}
|
|
53
|
+
export function isGrant(value) {
|
|
54
|
+
return isObject(value)
|
|
55
|
+
&& isString(value.id) && isString(value.issuer) && isString(value.issuerBaseUrl) && isString(value.audience)
|
|
56
|
+
&& Array.isArray(value.capabilities) && value.capabilities.length > 0 && value.capabilities.every(isCapability)
|
|
57
|
+
&& isString(value.issuedAt) && isString(value.expiresAt)
|
|
58
|
+
&& (value.parent === null || isString(value.parent))
|
|
59
|
+
&& isString(value.signature);
|
|
60
|
+
}
|
|
61
|
+
export function isAuthorization(value) {
|
|
62
|
+
return isObject(value)
|
|
63
|
+
&& isString(value.publicKey) && Array.isArray(value.grants) && isString(value.nonce) && isString(value.expiresAt) && isString(value.signature);
|
|
64
|
+
}
|
|
65
|
+
/** A `prefix.*` pattern's prefix, `''` for `*`, or undefined for an exact path. */
|
|
66
|
+
function wildcardPrefix(pattern) {
|
|
67
|
+
if (pattern === "*") {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
return pattern.endsWith(".*") ? pattern.slice(0, -2) : undefined;
|
|
71
|
+
}
|
|
72
|
+
/** Wildcards match on segment boundaries: `com.example.*` matches `com.example.sub` but not `com.examplefoo`. */
|
|
73
|
+
function underPrefix(path, prefix) {
|
|
74
|
+
return prefix === "" || path === prefix || path.startsWith(`${prefix}.`);
|
|
75
|
+
}
|
|
76
|
+
function pathMatches(pattern, path) {
|
|
77
|
+
const prefix = wildcardPrefix(pattern);
|
|
78
|
+
return prefix === undefined ? pattern === path : underPrefix(path, prefix);
|
|
79
|
+
}
|
|
80
|
+
/** True when every path `child` matches is also matched by `parent`. */
|
|
81
|
+
function pathCovers(parent, child) {
|
|
82
|
+
const parentPrefix = wildcardPrefix(parent);
|
|
83
|
+
if (parentPrefix === undefined) {
|
|
84
|
+
return parent === child;
|
|
85
|
+
}
|
|
86
|
+
const childPrefix = wildcardPrefix(child);
|
|
87
|
+
if (childPrefix === "") {
|
|
88
|
+
return parentPrefix === "";
|
|
89
|
+
}
|
|
90
|
+
return underPrefix(childPrefix ?? child, parentPrefix);
|
|
91
|
+
}
|
|
92
|
+
export function capabilityMatches(capability, resourcePath, resourceName, functionName, parameters) {
|
|
93
|
+
if (!pathMatches(capability.resourcePath, resourcePath)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (capability.resourceName !== undefined && capability.resourceName !== resourceName) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (capability.functionName !== undefined && capability.functionName !== functionName) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
// A constrained parameter must appear exactly once, so a duplicate can't carry a different value past the check.
|
|
103
|
+
return Object.entries(capability.constraints ?? {}).every(([name, value]) => {
|
|
104
|
+
const matching = parameters.filter(p => p.name === name);
|
|
105
|
+
return matching.length === 1 && sameJson(matching[0].value, value);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/** True when `child` allows nothing `parent` doesn't. A delegated grant may only narrow its parent. */
|
|
109
|
+
export function capabilityCovers(parent, child) {
|
|
110
|
+
if (!pathCovers(parent.resourcePath, child.resourcePath)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (parent.resourceName !== undefined && child.resourceName !== parent.resourceName) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (parent.functionName !== undefined && child.functionName !== parent.functionName) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
const childConstraints = child.constraints ?? {};
|
|
120
|
+
return Object.entries(parent.constraints ?? {}).every(([name, value]) => name in childConstraints && sameJson(childConstraints[name], value));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Checks a chain's signatures, expiries and links, root first. Revocation is the caller's
|
|
124
|
+
* job, since it needs the verifier's own records.
|
|
125
|
+
*/
|
|
126
|
+
export function verifyChain(grants, rootKey) {
|
|
127
|
+
if (!Array.isArray(grants) || grants.length === 0 || grants.length > MAX_GRANT_CHAIN_LENGTH) {
|
|
128
|
+
return { ok: false, reason: "chain length" };
|
|
129
|
+
}
|
|
130
|
+
if (!grants.every(isGrant)) {
|
|
131
|
+
return { ok: false, reason: "malformed grant" };
|
|
132
|
+
}
|
|
133
|
+
for (const [index, grant] of grants.entries()) {
|
|
134
|
+
if (!isFreshExpiry(grant.expiresAt, MAX_GRANT_LIFETIME_MS + CLOCK_SKEW_TOLERANCE_MS)) {
|
|
135
|
+
return { ok: false, reason: `grant ${index} expired` };
|
|
136
|
+
}
|
|
137
|
+
if (!safeVerifyData(grantPayload(grant), grant.issuer, grant.signature)) {
|
|
138
|
+
return { ok: false, reason: `grant ${index} signature` };
|
|
139
|
+
}
|
|
140
|
+
if (index === 0) {
|
|
141
|
+
if (grant.parent !== null || grant.issuer !== rootKey) {
|
|
142
|
+
return { ok: false, reason: "root issuer" };
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const parent = grants[index - 1];
|
|
147
|
+
if (grant.parent !== grantHash(parent) || grant.issuer !== parent.audience) {
|
|
148
|
+
return { ok: false, reason: `grant ${index} link` };
|
|
149
|
+
}
|
|
150
|
+
if (Date.parse(grant.expiresAt) > Date.parse(parent.expiresAt)) {
|
|
151
|
+
return { ok: false, reason: `grant ${index} outlives its parent` };
|
|
152
|
+
}
|
|
153
|
+
if (!grant.capabilities.every(child => parent.capabilities.some(capability => capabilityCovers(capability, child)))) {
|
|
154
|
+
return { ok: false, reason: `grant ${index} widens its parent` };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { ok: true, leaf: grants[grants.length - 1] };
|
|
158
|
+
}
|