@teikyo/access 0.5.0 → 0.6.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/README.md +6 -1
- package/lib/admin/handlers.d.ts +9 -0
- package/lib/admin/handlers.js +98 -0
- package/lib/admin/protocol.d.ts +151 -0
- package/lib/admin/protocol.js +119 -0
- package/lib/grants/cache.js +4 -1
- package/lib/grants/migrations.js +26 -0
- package/lib/grants/operators.d.ts +6 -0
- package/lib/grants/operators.js +26 -0
- package/lib/grants/plugin.d.ts +1 -0
- package/lib/grants/plugin.js +12 -5
- package/lib/grants/store.d.ts +33 -11
- package/lib/grants/store.js +127 -29
- package/lib/grants/tables.d.ts +4 -0
- package/lib/index.d.ts +6 -1
- package/lib/index.js +6 -1
- package/lib/replay/plugin.d.ts +11 -0
- package/lib/replay/plugin.js +28 -0
- package/lib/replay/store.d.ts +14 -0
- package/lib/replay/store.js +36 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -66,7 +66,12 @@ const cache = new GrantCache({ store })
|
|
|
66
66
|
await cache.allows('did:key:z6Mk...', 'app/read')
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
- `GrantStore` upserts grants by `(subject, pattern)`, normalises
|
|
69
|
+
- `GrantStore` upserts grants by `(created_by, subject, pattern)`, normalises DIDs and validates patterns.
|
|
70
|
+
- `grant` and `revoke` require `createdBy`, scope writes to that creator, and increment its set version.
|
|
71
|
+
- `replaceSet` atomically replaces one creator's grants using `expectedVersion`. Stale versions throw `GrantVersionConflictError` with `currentVersion`.
|
|
72
|
+
- `getSetVersion` returns zero before the first mutation. `replaceSet` can enforce `maxEntries`, throwing `GrantLimitError` without writes.
|
|
73
|
+
- `accessGrantsPlugin({ operators })` reconciles `config:operators` to one `admin/*` grant per configured DID during setup.
|
|
74
|
+
- `reconcileOperators(store, { dids, pattern? })` exposes the same reconciliation with an optional pattern.
|
|
70
75
|
- `GrantCache` holds active grants per subject for `ttlMs` (default 5000), or until the earliest grant expiry, and drops a subject's entry as soon as the store reports a change.
|
|
71
76
|
- `patternMatches(pattern, procedure)` uses the matcher Enkaku applies to access rules; `patternsOverlap(a, b)` reports whether two patterns can cover a common procedure.
|
|
72
77
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProcedureHandlers } from '@enkaku/server';
|
|
2
|
+
import type { AccessGrants } from '../grants/plugin.js';
|
|
3
|
+
import type { AccessAdminProtocol } from './protocol.js';
|
|
4
|
+
export type AccessAdminHandlersParams = {
|
|
5
|
+
grants: AccessGrants;
|
|
6
|
+
maxEntries?: number;
|
|
7
|
+
};
|
|
8
|
+
/** WARNING: mount behind Enkaku's `admin/*` grant check, because handlers rely on transport authorisation. */
|
|
9
|
+
export declare function createAccessAdminHandlers({ grants, maxEntries, }: AccessAdminHandlersParams): ProcedureHandlers<AccessAdminProtocol>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { HandlerError } from '@enkaku/server';
|
|
2
|
+
import { normalizeDID } from '@kokuin/token';
|
|
3
|
+
import { GrantLimitError, GrantVersionConflictError } from '../grants/store.js';
|
|
4
|
+
import { patternsOverlap } from '../patterns.js';
|
|
5
|
+
function issuer(payload) {
|
|
6
|
+
if (typeof payload.iss !== 'string') throw new HandlerError({
|
|
7
|
+
code: 'ACCESS_DENIED',
|
|
8
|
+
message: 'Verified caller required'
|
|
9
|
+
});
|
|
10
|
+
return payload.iss;
|
|
11
|
+
}
|
|
12
|
+
function assertGrantable(pattern) {
|
|
13
|
+
if (patternsOverlap(pattern, 'admin/*')) {
|
|
14
|
+
throw new HandlerError({
|
|
15
|
+
code: 'FORBIDDEN_PATTERN',
|
|
16
|
+
message: 'Admin patterns are configured by operators'
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function mutate(operation) {
|
|
21
|
+
try {
|
|
22
|
+
return await operation();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error instanceof GrantVersionConflictError) {
|
|
25
|
+
throw HandlerError.from(error, {
|
|
26
|
+
code: 'VERSION_CONFLICT',
|
|
27
|
+
data: {
|
|
28
|
+
currentVersion: error.currentVersion
|
|
29
|
+
},
|
|
30
|
+
message: error.message
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (error instanceof GrantLimitError) throw HandlerError.from(error, {
|
|
34
|
+
code: 'LIMIT',
|
|
35
|
+
message: error.message
|
|
36
|
+
});
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** WARNING: mount behind Enkaku's `admin/*` grant check, because handlers rely on transport authorisation. */ export function createAccessAdminHandlers({ grants, maxEntries }) {
|
|
41
|
+
const store = grants.store;
|
|
42
|
+
return {
|
|
43
|
+
'admin/grants/list': async ({ message, param })=>{
|
|
44
|
+
const createdBy = param.createdBy ?? issuer(message.payload);
|
|
45
|
+
return await store.listSet(createdBy);
|
|
46
|
+
},
|
|
47
|
+
'admin/grants/set': async ({ message, param })=>{
|
|
48
|
+
for (const entry of param.entries)assertGrantable(entry.pattern);
|
|
49
|
+
return await mutate(()=>{
|
|
50
|
+
return store.replaceSet({
|
|
51
|
+
...param,
|
|
52
|
+
createdBy: issuer(message.payload),
|
|
53
|
+
maxEntries
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
'admin/grants/add': async ({ message, param })=>{
|
|
58
|
+
assertGrantable(param.pattern);
|
|
59
|
+
const createdBy = issuer(message.payload);
|
|
60
|
+
// Use replaceSet so limits and version checks remain atomic with the mutation.
|
|
61
|
+
const version = await store.getSetVersion(createdBy);
|
|
62
|
+
const entries = (await store.list({
|
|
63
|
+
createdBy
|
|
64
|
+
})).filter((entry)=>{
|
|
65
|
+
return entry.subject !== normalizeDID(param.subject) || entry.pattern !== param.pattern;
|
|
66
|
+
}).map((entry)=>{
|
|
67
|
+
return {
|
|
68
|
+
subject: entry.subject,
|
|
69
|
+
pattern: entry.pattern,
|
|
70
|
+
...entry.expiresAt === null ? {} : {
|
|
71
|
+
expiresAt: entry.expiresAt
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
entries.push(param);
|
|
76
|
+
return await mutate(()=>{
|
|
77
|
+
return store.replaceSet({
|
|
78
|
+
createdBy,
|
|
79
|
+
entries,
|
|
80
|
+
expectedVersion: version,
|
|
81
|
+
maxEntries
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
'admin/grants/remove': async ({ message, param })=>{
|
|
86
|
+
assertGrantable(param.pattern);
|
|
87
|
+
const createdBy = issuer(message.payload);
|
|
88
|
+
await store.revoke({
|
|
89
|
+
createdBy,
|
|
90
|
+
subject: param.subject,
|
|
91
|
+
pattern: param.pattern
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
version: await store.getSetVersion(createdBy)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
export declare const accessAdminProtocol: {
|
|
2
|
+
readonly 'admin/grants/list': {
|
|
3
|
+
readonly type: "request";
|
|
4
|
+
readonly param: {
|
|
5
|
+
readonly type: "object";
|
|
6
|
+
readonly properties: {
|
|
7
|
+
readonly createdBy: {
|
|
8
|
+
readonly type: "string";
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
readonly additionalProperties: false;
|
|
12
|
+
};
|
|
13
|
+
readonly result: {
|
|
14
|
+
readonly type: "object";
|
|
15
|
+
readonly properties: {
|
|
16
|
+
readonly version: {
|
|
17
|
+
readonly type: "integer";
|
|
18
|
+
};
|
|
19
|
+
readonly entries: {
|
|
20
|
+
readonly type: "array";
|
|
21
|
+
readonly items: {
|
|
22
|
+
readonly type: "object";
|
|
23
|
+
readonly properties: {
|
|
24
|
+
readonly expiresAt: {
|
|
25
|
+
readonly type: readonly ["integer", "null"];
|
|
26
|
+
};
|
|
27
|
+
readonly createdBy: {
|
|
28
|
+
readonly type: "string";
|
|
29
|
+
};
|
|
30
|
+
readonly createdAt: {
|
|
31
|
+
readonly type: "integer";
|
|
32
|
+
};
|
|
33
|
+
readonly subject: {
|
|
34
|
+
readonly type: "string";
|
|
35
|
+
};
|
|
36
|
+
readonly pattern: {
|
|
37
|
+
readonly type: "string";
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
readonly required: readonly ["subject", "pattern", "expiresAt", "createdBy", "createdAt"];
|
|
41
|
+
readonly additionalProperties: false;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
readonly required: readonly ["entries", "version"];
|
|
46
|
+
readonly additionalProperties: false;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
readonly 'admin/grants/set': {
|
|
50
|
+
readonly type: "request";
|
|
51
|
+
readonly param: {
|
|
52
|
+
readonly type: "object";
|
|
53
|
+
readonly properties: {
|
|
54
|
+
readonly entries: {
|
|
55
|
+
readonly type: "array";
|
|
56
|
+
readonly items: {
|
|
57
|
+
readonly type: "object";
|
|
58
|
+
readonly properties: {
|
|
59
|
+
readonly subject: {
|
|
60
|
+
readonly type: "string";
|
|
61
|
+
};
|
|
62
|
+
readonly pattern: {
|
|
63
|
+
readonly type: "string";
|
|
64
|
+
};
|
|
65
|
+
readonly expiresAt: {
|
|
66
|
+
readonly type: "integer";
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
readonly required: readonly ["subject", "pattern"];
|
|
70
|
+
readonly additionalProperties: false;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
readonly expectedVersion: {
|
|
74
|
+
readonly type: "integer";
|
|
75
|
+
readonly minimum: 0;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
readonly required: readonly ["entries", "expectedVersion"];
|
|
79
|
+
readonly additionalProperties: false;
|
|
80
|
+
};
|
|
81
|
+
readonly result: {
|
|
82
|
+
readonly type: "object";
|
|
83
|
+
readonly properties: {
|
|
84
|
+
readonly version: {
|
|
85
|
+
readonly type: "integer";
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
readonly required: readonly ["version"];
|
|
89
|
+
readonly additionalProperties: false;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
readonly 'admin/grants/add': {
|
|
93
|
+
readonly type: "request";
|
|
94
|
+
readonly param: {
|
|
95
|
+
readonly type: "object";
|
|
96
|
+
readonly properties: {
|
|
97
|
+
readonly subject: {
|
|
98
|
+
readonly type: "string";
|
|
99
|
+
};
|
|
100
|
+
readonly pattern: {
|
|
101
|
+
readonly type: "string";
|
|
102
|
+
};
|
|
103
|
+
readonly expiresAt: {
|
|
104
|
+
readonly type: "integer";
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
readonly required: readonly ["subject", "pattern"];
|
|
108
|
+
readonly additionalProperties: false;
|
|
109
|
+
};
|
|
110
|
+
readonly result: {
|
|
111
|
+
readonly type: "object";
|
|
112
|
+
readonly properties: {
|
|
113
|
+
readonly version: {
|
|
114
|
+
readonly type: "integer";
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
readonly required: readonly ["version"];
|
|
118
|
+
readonly additionalProperties: false;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
readonly 'admin/grants/remove': {
|
|
122
|
+
readonly type: "request";
|
|
123
|
+
readonly param: {
|
|
124
|
+
readonly type: "object";
|
|
125
|
+
readonly properties: {
|
|
126
|
+
readonly subject: {
|
|
127
|
+
readonly type: "string";
|
|
128
|
+
};
|
|
129
|
+
readonly pattern: {
|
|
130
|
+
readonly type: "string";
|
|
131
|
+
};
|
|
132
|
+
readonly expiresAt: {
|
|
133
|
+
readonly type: "integer";
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
readonly required: readonly ["subject", "pattern"];
|
|
137
|
+
readonly additionalProperties: false;
|
|
138
|
+
};
|
|
139
|
+
readonly result: {
|
|
140
|
+
readonly type: "object";
|
|
141
|
+
readonly properties: {
|
|
142
|
+
readonly version: {
|
|
143
|
+
readonly type: "integer";
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
readonly required: readonly ["version"];
|
|
147
|
+
readonly additionalProperties: false;
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
export type AccessAdminProtocol = typeof accessAdminProtocol;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const entry = {
|
|
2
|
+
type: 'object',
|
|
3
|
+
properties: {
|
|
4
|
+
subject: {
|
|
5
|
+
type: 'string'
|
|
6
|
+
},
|
|
7
|
+
pattern: {
|
|
8
|
+
type: 'string'
|
|
9
|
+
},
|
|
10
|
+
expiresAt: {
|
|
11
|
+
type: 'integer'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
required: [
|
|
15
|
+
'subject',
|
|
16
|
+
'pattern'
|
|
17
|
+
],
|
|
18
|
+
additionalProperties: false
|
|
19
|
+
};
|
|
20
|
+
const version = {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
version: {
|
|
24
|
+
type: 'integer'
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
required: [
|
|
28
|
+
'version'
|
|
29
|
+
],
|
|
30
|
+
additionalProperties: false
|
|
31
|
+
};
|
|
32
|
+
export const accessAdminProtocol = {
|
|
33
|
+
'admin/grants/list': {
|
|
34
|
+
type: 'request',
|
|
35
|
+
param: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
createdBy: {
|
|
39
|
+
type: 'string'
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
additionalProperties: false
|
|
43
|
+
},
|
|
44
|
+
result: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
version: {
|
|
48
|
+
type: 'integer'
|
|
49
|
+
},
|
|
50
|
+
entries: {
|
|
51
|
+
type: 'array',
|
|
52
|
+
items: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
...entry.properties,
|
|
56
|
+
expiresAt: {
|
|
57
|
+
type: [
|
|
58
|
+
'integer',
|
|
59
|
+
'null'
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
createdBy: {
|
|
63
|
+
type: 'string'
|
|
64
|
+
},
|
|
65
|
+
createdAt: {
|
|
66
|
+
type: 'integer'
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
required: [
|
|
70
|
+
'subject',
|
|
71
|
+
'pattern',
|
|
72
|
+
'expiresAt',
|
|
73
|
+
'createdBy',
|
|
74
|
+
'createdAt'
|
|
75
|
+
],
|
|
76
|
+
additionalProperties: false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
required: [
|
|
81
|
+
'entries',
|
|
82
|
+
'version'
|
|
83
|
+
],
|
|
84
|
+
additionalProperties: false
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
'admin/grants/set': {
|
|
88
|
+
type: 'request',
|
|
89
|
+
param: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
entries: {
|
|
93
|
+
type: 'array',
|
|
94
|
+
items: entry
|
|
95
|
+
},
|
|
96
|
+
expectedVersion: {
|
|
97
|
+
type: 'integer',
|
|
98
|
+
minimum: 0
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
required: [
|
|
102
|
+
'entries',
|
|
103
|
+
'expectedVersion'
|
|
104
|
+
],
|
|
105
|
+
additionalProperties: false
|
|
106
|
+
},
|
|
107
|
+
result: version
|
|
108
|
+
},
|
|
109
|
+
'admin/grants/add': {
|
|
110
|
+
type: 'request',
|
|
111
|
+
param: entry,
|
|
112
|
+
result: version
|
|
113
|
+
},
|
|
114
|
+
'admin/grants/remove': {
|
|
115
|
+
type: 'request',
|
|
116
|
+
param: entry,
|
|
117
|
+
result: version
|
|
118
|
+
}
|
|
119
|
+
};
|
package/lib/grants/cache.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeDID } from '@kokuin/token';
|
|
2
2
|
import { patternMatches } from '../patterns.js';
|
|
3
|
+
import { OPERATORS_CREATOR_ID } from './operators.js';
|
|
3
4
|
const DEFAULT_TTL_MS = 5000;
|
|
4
5
|
const DEFAULT_MAX_ENTRIES = 10_000;
|
|
5
6
|
/** Whether `ttlMs` is a usable cache lifetime: a non-negative safe integer (0 disables caching). */ export function isValidTTL(ttlMs) {
|
|
@@ -92,7 +93,9 @@ const DEFAULT_MAX_ENTRIES = 10_000;
|
|
|
92
93
|
throw new Error('GrantCache is disposed');
|
|
93
94
|
}
|
|
94
95
|
const grants = await this.#grantsFor(normalizeDID(subject));
|
|
95
|
-
return grants.some((grant)=>
|
|
96
|
+
return grants.some((grant)=>{
|
|
97
|
+
return (!procedure.startsWith('admin/') || grant.createdBy === OPERATORS_CREATOR_ID) && patternMatches(grant.pattern, procedure);
|
|
98
|
+
});
|
|
96
99
|
}
|
|
97
100
|
/** Stop listening to the store and drop all cached state. */ dispose() {
|
|
98
101
|
this.#disposed = true;
|
package/lib/grants/migrations.js
CHANGED
|
@@ -11,6 +11,32 @@ export function grantStoreMigrations(ctx) {
|
|
|
11
11
|
async down (db) {
|
|
12
12
|
await db.schema.dropTable('grants').execute();
|
|
13
13
|
}
|
|
14
|
+
},
|
|
15
|
+
'1-owner-key': {
|
|
16
|
+
async up (db) {
|
|
17
|
+
await db.schema.createTable('grants_owned').addColumn('subject', ctx.types.text, (column)=>column.notNull()).addColumn('pattern', ctx.types.text, (column)=>column.notNull()).addColumn('expires_at', ctx.types.bigint).addColumn('created_at', ctx.types.bigint, (column)=>column.notNull()).addColumn('created_by', ctx.types.text, (column)=>column.notNull()).addPrimaryKeyConstraint(`${ctx.tablePrefix}_grants_owner_pkey`, [
|
|
18
|
+
'created_by',
|
|
19
|
+
'subject',
|
|
20
|
+
'pattern'
|
|
21
|
+
]).execute();
|
|
22
|
+
await db.insertInto('grants_owned').columns([
|
|
23
|
+
'subject',
|
|
24
|
+
'pattern',
|
|
25
|
+
'expires_at',
|
|
26
|
+
'created_at',
|
|
27
|
+
'created_by'
|
|
28
|
+
]).expression(db.selectFrom('grants').select([
|
|
29
|
+
'subject',
|
|
30
|
+
'pattern',
|
|
31
|
+
'expires_at',
|
|
32
|
+
'created_at',
|
|
33
|
+
'created_by'
|
|
34
|
+
])).execute();
|
|
35
|
+
await db.schema.dropTable('grants').execute();
|
|
36
|
+
await db.schema.alterTable('grants_owned').renameTo('grants').execute();
|
|
37
|
+
await db.schema.createIndex(`${ctx.tablePrefix}_grants_subject`).on('grants').column('subject').execute();
|
|
38
|
+
await db.schema.createTable('grant_sets').addColumn('created_by', ctx.types.text, (column)=>column.notNull().primaryKey()).addColumn('version', 'integer', (column)=>column.notNull()).execute();
|
|
39
|
+
}
|
|
14
40
|
}
|
|
15
41
|
};
|
|
16
42
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
import { GrantVersionConflictError } from './store.js';
|
|
3
|
+
export const OPERATORS_CREATOR_ID = 'config:operators';
|
|
4
|
+
export async function reconcileOperators(store, params) {
|
|
5
|
+
const entries = [
|
|
6
|
+
...new Set(params.dids.map(normalizeDID))
|
|
7
|
+
].map((subject)=>{
|
|
8
|
+
return {
|
|
9
|
+
subject,
|
|
10
|
+
pattern: params.pattern ?? 'admin/*'
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
for(let attempt = 0;; attempt++){
|
|
14
|
+
const expectedVersion = await store.getSetVersion(OPERATORS_CREATOR_ID);
|
|
15
|
+
try {
|
|
16
|
+
await store.replaceSet({
|
|
17
|
+
createdBy: OPERATORS_CREATOR_ID,
|
|
18
|
+
entries,
|
|
19
|
+
expectedVersion
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (!(error instanceof GrantVersionConflictError) || attempt >= 4) throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
package/lib/grants/plugin.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type AccessGrants = {
|
|
|
11
11
|
};
|
|
12
12
|
export declare const ACCESS_GRANTS: import("@sozai/http-server").PluginName<"access:grants", AccessGrants>;
|
|
13
13
|
export type AccessGrantsPluginParams = {
|
|
14
|
+
operators?: Array<string>;
|
|
14
15
|
/** Maximum age of cached grants in milliseconds, a non-negative integer (0 disables caching). */
|
|
15
16
|
cacheTTLMs?: number;
|
|
16
17
|
};
|
package/lib/grants/plugin.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
1
2
|
import { pluginName } from '@sozai/http-server';
|
|
2
3
|
import { HOZON_DB } from '@teikyo/hozon';
|
|
3
4
|
import { GrantCache, isValidTTL } from './cache.js';
|
|
5
|
+
import { reconcileOperators } from './operators.js';
|
|
4
6
|
import { GRANT_STORE, grantStoreDefinition } from './store.js';
|
|
5
7
|
export const ACCESS_GRANTS = pluginName()('access:grants');
|
|
6
8
|
/** Provide the grant store and an `allow()` predicate backed by a grant cache. */ export function accessGrantsPlugin(params = {}) {
|
|
@@ -16,6 +18,9 @@ export const ACCESS_GRANTS = pluginName()('access:grants');
|
|
|
16
18
|
const db = ctx.use(HOZON_DB);
|
|
17
19
|
db.register(grantStoreDefinition);
|
|
18
20
|
const store = await db.getStore(GRANT_STORE);
|
|
21
|
+
await reconcileOperators(store, {
|
|
22
|
+
dids: params.operators ?? []
|
|
23
|
+
});
|
|
19
24
|
const cache = new GrantCache({
|
|
20
25
|
store,
|
|
21
26
|
ttlMs: params.cacheTTLMs
|
|
@@ -36,12 +41,13 @@ export const ACCESS_GRANTS = pluginName()('access:grants');
|
|
|
36
41
|
});
|
|
37
42
|
}
|
|
38
43
|
},
|
|
39
|
-
allow: ()=>
|
|
44
|
+
allow: ()=>{
|
|
45
|
+
return async ({ payload, procedure, verifyDelegation })=>{
|
|
40
46
|
try {
|
|
41
|
-
if (
|
|
42
|
-
return
|
|
47
|
+
if (payload.sub == null || normalizeDID(payload.sub) === normalizeDID(payload.iss)) {
|
|
48
|
+
return await cache.allows(payload.iss, procedure);
|
|
43
49
|
}
|
|
44
|
-
if (
|
|
50
|
+
if (await cache.allows(payload.sub, procedure)) {
|
|
45
51
|
return await verifyDelegation();
|
|
46
52
|
}
|
|
47
53
|
return false;
|
|
@@ -54,7 +60,8 @@ export const ACCESS_GRANTS = pluginName()('access:grants');
|
|
|
54
60
|
cause
|
|
55
61
|
});
|
|
56
62
|
}
|
|
57
|
-
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
58
65
|
};
|
|
59
66
|
}
|
|
60
67
|
};
|
package/lib/grants/store.d.ts
CHANGED
|
@@ -14,27 +14,49 @@ export type GrantParams = {
|
|
|
14
14
|
expiresAt?: number;
|
|
15
15
|
};
|
|
16
16
|
export type RevokeParams = {
|
|
17
|
+
createdBy: string;
|
|
17
18
|
subject: string;
|
|
18
19
|
pattern?: string;
|
|
19
20
|
};
|
|
21
|
+
export type ReplaceGrantSetParams = {
|
|
22
|
+
createdBy: string;
|
|
23
|
+
entries: Array<{
|
|
24
|
+
subject: string;
|
|
25
|
+
pattern: string;
|
|
26
|
+
expiresAt?: number;
|
|
27
|
+
}>;
|
|
28
|
+
expectedVersion: number;
|
|
29
|
+
maxEntries?: number;
|
|
30
|
+
};
|
|
31
|
+
export type GrantVersionConflictErrorParams = {
|
|
32
|
+
currentVersion: number;
|
|
33
|
+
};
|
|
34
|
+
export declare class GrantVersionConflictError extends Error {
|
|
35
|
+
currentVersion: number;
|
|
36
|
+
constructor(params: GrantVersionConflictErrorParams);
|
|
37
|
+
}
|
|
38
|
+
export declare class GrantLimitError extends Error {
|
|
39
|
+
constructor();
|
|
40
|
+
}
|
|
20
41
|
export type GrantStore = {
|
|
21
|
-
/**
|
|
22
|
-
* Add a grant, or replace the expiry and audit fields of an existing grant for the same subject
|
|
23
|
-
* and pattern -- the latest grant wins. Throws inside a database transaction, because listeners
|
|
24
|
-
* could not be notified after the commit.
|
|
25
|
-
*/
|
|
42
|
+
/** WARNING: use a top-level store for mutations, because nested transactions cannot commit independently. */
|
|
26
43
|
grant(params: GrantParams): Promise<void>;
|
|
27
|
-
/**
|
|
28
|
-
* Remove one grant, or all grants of the subject when no pattern is given. Returns the rows
|
|
29
|
-
* removed. Throws inside a database transaction.
|
|
30
|
-
*/
|
|
31
44
|
revoke(params: RevokeParams): Promise<number>;
|
|
45
|
+
replaceSet(params: ReplaceGrantSetParams): Promise<{
|
|
46
|
+
version: number;
|
|
47
|
+
}>;
|
|
48
|
+
getSetVersion(createdBy: string): Promise<number>;
|
|
49
|
+
/** Entries and version from one database snapshot, including expired grants. */
|
|
50
|
+
listSet(createdBy: string): Promise<{
|
|
51
|
+
entries: Array<Grant>;
|
|
52
|
+
version: number;
|
|
53
|
+
}>;
|
|
32
54
|
list(params?: {
|
|
33
55
|
subject?: string;
|
|
56
|
+
createdBy?: string;
|
|
34
57
|
}): Promise<Array<Grant>>;
|
|
35
|
-
/** Grants of the subject that have not expired at the given time. */
|
|
36
58
|
activeGrants(subject: string, now: number): Promise<Array<Grant>>;
|
|
37
|
-
/**
|
|
59
|
+
/** Called after commit, once for each affected subject. */
|
|
38
60
|
onChange(listener: (subject: string) => void): () => void;
|
|
39
61
|
};
|
|
40
62
|
export declare function createGrantStoreAPI(db: Kysely<GrantTables>): GrantStore;
|
package/lib/grants/store.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
+
import { withStoreTransaction } from '@hozon/db';
|
|
1
2
|
import { assertValidPattern } from '@kokuin/capability';
|
|
2
3
|
import { normalizeDID } from '@kokuin/token';
|
|
3
4
|
import { getLogger } from '@sozai/log';
|
|
4
5
|
import { grantStoreMigrations } from './migrations.js';
|
|
6
|
+
export class GrantVersionConflictError extends Error {
|
|
7
|
+
currentVersion;
|
|
8
|
+
constructor(params){
|
|
9
|
+
super('Grant set version conflict');
|
|
10
|
+
this.name = 'GrantVersionConflictError';
|
|
11
|
+
this.currentVersion = params.currentVersion;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class GrantLimitError extends Error {
|
|
15
|
+
constructor(){
|
|
16
|
+
super('Grant set limit exceeded');
|
|
17
|
+
this.name = 'GrantLimitError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
5
20
|
function toGrant(row) {
|
|
6
21
|
return {
|
|
7
22
|
subject: row.subject,
|
|
@@ -37,49 +52,132 @@ export function createGrantStoreAPI(db) {
|
|
|
37
52
|
}
|
|
38
53
|
}
|
|
39
54
|
}
|
|
55
|
+
async function lockVersion(tx, createdBy) {
|
|
56
|
+
await tx.insertInto('grant_sets').values({
|
|
57
|
+
created_by: createdBy,
|
|
58
|
+
version: 0
|
|
59
|
+
}).onConflict((conflict)=>conflict.column('created_by').doNothing()).execute();
|
|
60
|
+
// Reserve SQLite's writer and lock the Postgres row before reading the version.
|
|
61
|
+
const row = await tx.updateTable('grant_sets').set((eb)=>({
|
|
62
|
+
version: eb.ref('version')
|
|
63
|
+
})).where('created_by', '=', createdBy).returning('version').executeTakeFirstOrThrow();
|
|
64
|
+
return Number(row.version);
|
|
65
|
+
}
|
|
66
|
+
async function bumpVersion(tx, createdBy, version) {
|
|
67
|
+
await tx.updateTable('grant_sets').set({
|
|
68
|
+
version: version + 1
|
|
69
|
+
}).where('created_by', '=', createdBy).execute();
|
|
70
|
+
}
|
|
71
|
+
function grantRow(params) {
|
|
72
|
+
assertValidPattern(params.pattern);
|
|
73
|
+
if (params.expiresAt !== undefined && !Number.isSafeInteger(params.expiresAt)) {
|
|
74
|
+
throw new Error('Invalid grant expiry');
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
subject: normalizeDID(params.subject),
|
|
78
|
+
pattern: params.pattern,
|
|
79
|
+
expires_at: params.expiresAt ?? null,
|
|
80
|
+
created_at: Date.now(),
|
|
81
|
+
created_by: normalizeDID(params.createdBy)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
40
84
|
return {
|
|
41
85
|
async grant (params) {
|
|
42
86
|
assertTopLevel();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
created_by
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
'pattern'
|
|
60
|
-
]).doUpdateSet({
|
|
61
|
-
expires_at: expiresAt,
|
|
62
|
-
created_at: createdAt,
|
|
63
|
-
created_by: createdBy
|
|
64
|
-
})).execute();
|
|
65
|
-
notify(subject);
|
|
87
|
+
const row = grantRow(params);
|
|
88
|
+
await withStoreTransaction(db, async (tx)=>{
|
|
89
|
+
const version = await lockVersion(tx, row.created_by);
|
|
90
|
+
await tx.insertInto('grants').values(row).onConflict((conflict)=>{
|
|
91
|
+
return conflict.columns([
|
|
92
|
+
'created_by',
|
|
93
|
+
'subject',
|
|
94
|
+
'pattern'
|
|
95
|
+
]).doUpdateSet({
|
|
96
|
+
expires_at: row.expires_at,
|
|
97
|
+
created_at: row.created_at
|
|
98
|
+
});
|
|
99
|
+
}).execute();
|
|
100
|
+
await bumpVersion(tx, row.created_by, version);
|
|
101
|
+
});
|
|
102
|
+
notify(row.subject);
|
|
66
103
|
},
|
|
67
104
|
async revoke (params) {
|
|
68
105
|
assertTopLevel();
|
|
69
106
|
const subject = normalizeDID(params.subject);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
107
|
+
const createdBy = normalizeDID(params.createdBy);
|
|
108
|
+
const removed = await withStoreTransaction(db, async (tx)=>{
|
|
109
|
+
const version = await lockVersion(tx, createdBy);
|
|
110
|
+
let query = tx.deleteFrom('grants').where('subject', '=', subject).where('created_by', '=', createdBy);
|
|
111
|
+
if (params.pattern != null) query = query.where('pattern', '=', params.pattern);
|
|
112
|
+
const result = await query.executeTakeFirst();
|
|
113
|
+
await bumpVersion(tx, createdBy, version);
|
|
114
|
+
return Number(result.numDeletedRows);
|
|
115
|
+
});
|
|
116
|
+
if (removed > 0) notify(subject);
|
|
117
|
+
return removed;
|
|
118
|
+
},
|
|
119
|
+
async replaceSet (params) {
|
|
120
|
+
assertTopLevel();
|
|
121
|
+
const createdBy = normalizeDID(params.createdBy);
|
|
122
|
+
const result = await withStoreTransaction(db, async (tx)=>{
|
|
123
|
+
const version = await lockVersion(tx, createdBy);
|
|
124
|
+
if (version !== params.expectedVersion) throw new GrantVersionConflictError({
|
|
125
|
+
currentVersion: version
|
|
126
|
+
});
|
|
127
|
+
if (params.maxEntries !== undefined && params.entries.length > params.maxEntries) throw new GrantLimitError();
|
|
128
|
+
const rows = params.entries.map((entry)=>grantRow({
|
|
129
|
+
...entry,
|
|
130
|
+
createdBy
|
|
131
|
+
}));
|
|
132
|
+
const previous = await tx.selectFrom('grants').select('subject').where('created_by', '=', createdBy).execute();
|
|
133
|
+
await tx.deleteFrom('grants').where('created_by', '=', createdBy).execute();
|
|
134
|
+
for (const row of rows)await tx.insertInto('grants').values(row).execute();
|
|
135
|
+
await bumpVersion(tx, createdBy, version);
|
|
136
|
+
return {
|
|
137
|
+
version: version + 1,
|
|
138
|
+
subjects: new Set([
|
|
139
|
+
...previous,
|
|
140
|
+
...rows
|
|
141
|
+
].map((row)=>row.subject))
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
for (const subject of result.subjects)notify(subject);
|
|
145
|
+
return {
|
|
146
|
+
version: result.version
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
async getSetVersion (createdBy) {
|
|
150
|
+
const row = await db.selectFrom('grant_sets').select('version').where('created_by', '=', normalizeDID(createdBy)).executeTakeFirst();
|
|
151
|
+
return row == null ? 0 : Number(row.version);
|
|
152
|
+
},
|
|
153
|
+
async listSet (createdBy) {
|
|
154
|
+
// One statement keeps entries and version consistent even under READ COMMITTED.
|
|
155
|
+
const rows = await db.selectFrom(db.selectNoFrom((eb)=>eb.val(normalizeDID(createdBy)).as('created_by')).as('creator')).leftJoin('grant_sets', 'grant_sets.created_by', 'creator.created_by').leftJoin('grants', 'grants.created_by', 'creator.created_by').selectAll('grants').select('grant_sets.version').orderBy('grants.created_at').orderBy('grants.pattern').execute();
|
|
156
|
+
const entries = [];
|
|
157
|
+
for (const row of rows){
|
|
158
|
+
if (row.subject !== null && row.pattern !== null && row.created_at !== null && row.created_by !== null) {
|
|
159
|
+
entries.push(toGrant({
|
|
160
|
+
subject: row.subject,
|
|
161
|
+
pattern: row.pattern,
|
|
162
|
+
expires_at: row.expires_at,
|
|
163
|
+
created_at: row.created_at,
|
|
164
|
+
created_by: row.created_by
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
73
167
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
168
|
+
return {
|
|
169
|
+
entries,
|
|
170
|
+
version: Number(rows[0]?.version ?? 0)
|
|
171
|
+
};
|
|
77
172
|
},
|
|
78
173
|
async list (params) {
|
|
79
174
|
let query = db.selectFrom('grants').selectAll().orderBy('created_at').orderBy('pattern');
|
|
80
175
|
if (params?.subject != null) {
|
|
81
176
|
query = query.where('subject', '=', normalizeDID(params.subject));
|
|
82
177
|
}
|
|
178
|
+
if (params?.createdBy != null) {
|
|
179
|
+
query = query.where('created_by', '=', normalizeDID(params.createdBy));
|
|
180
|
+
}
|
|
83
181
|
return (await query.execute()).map(toGrant);
|
|
84
182
|
},
|
|
85
183
|
async activeGrants (subject, now) {
|
package/lib/grants/tables.d.ts
CHANGED
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
export { type AccessAdminHandlersParams, createAccessAdminHandlers } from './admin/handlers.js';
|
|
2
|
+
export { type AccessAdminProtocol, accessAdminProtocol } from './admin/protocol.js';
|
|
1
3
|
export { GrantCache, type GrantCacheParams } from './grants/cache.js';
|
|
4
|
+
export { OPERATORS_CREATOR_ID, reconcileOperators } from './grants/operators.js';
|
|
2
5
|
export { ACCESS_GRANTS, type AccessGrants, type AccessGrantsPluginParams, accessGrantsPlugin, } from './grants/plugin.js';
|
|
3
|
-
export { GRANT_STORE, type Grant, type GrantParams, type GrantStore, grantStoreDefinition, type RevokeParams, } from './grants/store.js';
|
|
6
|
+
export { GRANT_STORE, type Grant, GrantLimitError, type GrantParams, type GrantStore, GrantVersionConflictError, type GrantVersionConflictErrorParams, grantStoreDefinition, type ReplaceGrantSetParams, type RevokeParams, } from './grants/store.js';
|
|
4
7
|
export type { GrantTables } from './grants/tables.js';
|
|
5
8
|
export { patternMatches, patternsOverlap } from './patterns.js';
|
|
9
|
+
export { ACCESS_REPLAY, type AccessReplay, type AccessReplayPluginParams, accessReplayPlugin, } from './replay/plugin.js';
|
|
10
|
+
export { REPLAY_STORE, type ReplayStore, type ReplayTables, replayStoreDefinition, } from './replay/store.js';
|
|
6
11
|
export { createRevocationHook, createRevocationStoreDefinition, REVOCATION_STORE, type RevocationHookParams, type RevocationStoreAPI, type RevocationStoreParams, } from './revocation/backend.js';
|
|
7
12
|
export { ACCESS_REVOCATION, type AccessRevocation, type AccessRevocationPluginParams, accessRevocationPlugin, } from './revocation/plugin.js';
|
|
8
13
|
export type { RevocationTables } from './revocation/tables.js';
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
export { createAccessAdminHandlers } from './admin/handlers.js';
|
|
2
|
+
export { accessAdminProtocol } from './admin/protocol.js';
|
|
1
3
|
export { GrantCache } from './grants/cache.js';
|
|
4
|
+
export { OPERATORS_CREATOR_ID, reconcileOperators } from './grants/operators.js';
|
|
2
5
|
export { ACCESS_GRANTS, accessGrantsPlugin } from './grants/plugin.js';
|
|
3
|
-
export { GRANT_STORE, grantStoreDefinition } from './grants/store.js';
|
|
6
|
+
export { GRANT_STORE, GrantLimitError, GrantVersionConflictError, grantStoreDefinition } from './grants/store.js';
|
|
4
7
|
export { patternMatches, patternsOverlap } from './patterns.js';
|
|
8
|
+
export { ACCESS_REPLAY, accessReplayPlugin } from './replay/plugin.js';
|
|
9
|
+
export { REPLAY_STORE, replayStoreDefinition } from './replay/store.js';
|
|
5
10
|
export { createRevocationHook, createRevocationStoreDefinition, REVOCATION_STORE } from './revocation/backend.js';
|
|
6
11
|
export { ACCESS_REVOCATION, accessRevocationPlugin } from './revocation/plugin.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReplayCache } from '@enkaku/server';
|
|
2
|
+
import type { HTTPPlugin } from '@sozai/http-server';
|
|
3
|
+
import { HOZON_DB } from '@teikyo/hozon';
|
|
4
|
+
export type AccessReplay = {
|
|
5
|
+
cache: ReplayCache;
|
|
6
|
+
};
|
|
7
|
+
export declare const ACCESS_REPLAY: import("@sozai/http-server").PluginName<"access:replay", AccessReplay>;
|
|
8
|
+
export type AccessReplayPluginParams = {
|
|
9
|
+
cleanupIntervalMs?: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function accessReplayPlugin(params?: AccessReplayPluginParams): HTTPPlugin<'access:replay', AccessReplay, [typeof HOZON_DB]>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { pluginName } from '@sozai/http-server';
|
|
2
|
+
import { HOZON_DB } from '@teikyo/hozon';
|
|
3
|
+
import { REPLAY_STORE, replayStoreDefinition } from './store.js';
|
|
4
|
+
export const ACCESS_REPLAY = pluginName()('access:replay');
|
|
5
|
+
export function accessReplayPlugin(params = {}) {
|
|
6
|
+
return {
|
|
7
|
+
name: ACCESS_REPLAY,
|
|
8
|
+
dependsOn: [
|
|
9
|
+
HOZON_DB
|
|
10
|
+
],
|
|
11
|
+
async setup (ctx) {
|
|
12
|
+
const db = ctx.use(HOZON_DB);
|
|
13
|
+
db.register(replayStoreDefinition);
|
|
14
|
+
const store = await db.getStore(REPLAY_STORE);
|
|
15
|
+
const timer = setInterval(()=>{
|
|
16
|
+
void store.cleanup().catch((error)=>{
|
|
17
|
+
ctx.logger.error('replay cleanup failed', {
|
|
18
|
+
error
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}, params.cleanupIntervalMs ?? 60_000);
|
|
22
|
+
ctx.onShutdown(()=>clearInterval(timer));
|
|
23
|
+
return {
|
|
24
|
+
cache: store
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReplayCache } from '@enkaku/server';
|
|
2
|
+
import type { Kysely, StoreDefinition } from '@hozon/db';
|
|
3
|
+
export type ReplayTables = {
|
|
4
|
+
replay_keys: {
|
|
5
|
+
key: string;
|
|
6
|
+
expires_at: number;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export type ReplayStore = ReplayCache & {
|
|
10
|
+
cleanup(): Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
export declare const REPLAY_STORE = "teikyo-replay";
|
|
13
|
+
export declare function createReplayStoreAPI(db: Kysely<ReplayTables>): ReplayStore;
|
|
14
|
+
export declare const replayStoreDefinition: StoreDefinition<ReplayTables, ReplayStore>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { withStoreTransaction } from '@hozon/db';
|
|
2
|
+
export const REPLAY_STORE = 'teikyo-replay';
|
|
3
|
+
export function createReplayStoreAPI(db) {
|
|
4
|
+
return {
|
|
5
|
+
async checkAndRecord (key, expiresAt) {
|
|
6
|
+
return await withStoreTransaction(db, async (tx)=>{
|
|
7
|
+
// Enkaku accepts tokens at expiry equality, so retain their replay records too.
|
|
8
|
+
await tx.deleteFrom('replay_keys').where('key', '=', key).where('expires_at', '<', Date.now()).execute();
|
|
9
|
+
const result = await tx.insertInto('replay_keys').values({
|
|
10
|
+
key,
|
|
11
|
+
expires_at: expiresAt
|
|
12
|
+
}).onConflict((conflict)=>conflict.column('key').doNothing()).executeTakeFirst();
|
|
13
|
+
return Number(result.numInsertedOrUpdatedRows) === 1;
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
async cleanup () {
|
|
17
|
+
await db.deleteFrom('replay_keys').where('expires_at', '<', Date.now()).execute();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export const replayStoreDefinition = {
|
|
22
|
+
name: REPLAY_STORE,
|
|
23
|
+
migrations: (ctx)=>{
|
|
24
|
+
return {
|
|
25
|
+
'0-init': {
|
|
26
|
+
async up (db) {
|
|
27
|
+
await db.schema.createTable('replay_keys').addColumn('key', ctx.types.text, (column)=>column.notNull().primaryKey()).addColumn('expires_at', ctx.types.bigint, (column)=>column.notNull()).execute();
|
|
28
|
+
},
|
|
29
|
+
async down (db) {
|
|
30
|
+
await db.schema.dropTable('replay_keys').execute();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
createAPI: createReplayStoreAPI
|
|
36
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teikyo/access",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Access grants and revocation for Enkaku servers",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"lib/*"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@enkaku/
|
|
22
|
+
"@enkaku/protocol": "^0.23.1",
|
|
23
|
+
"@enkaku/server": "^0.23.1",
|
|
23
24
|
"@hozon/db": "^0.5.1",
|
|
24
25
|
"@kokuin/capability": "^0.7.0",
|
|
25
26
|
"@kokuin/token": "^0.7.0",
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@hozon/provider": "^0.3.1",
|
|
33
|
-
"@kokuin/crypto": "^0.7.
|
|
34
|
+
"@kokuin/crypto": "^0.7.1",
|
|
34
35
|
"@types/node": "^26.6.6",
|
|
35
36
|
"del-cli": "^8.0.1",
|
|
36
37
|
"vitest": "5.1.0"
|