@oxyhq/core 20.0.0 → 20.1.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/NOTICE +10 -9
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/mixins/OxyServices.chains.js +73 -0
- package/dist/cjs/mixins/OxyServices.store.js +266 -0
- package/dist/cjs/mixins/OxyServices.utility.js +159 -104
- package/dist/cjs/mixins/index.js +7 -0
- package/dist/cjs/server/rateLimit.js +15 -6
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/mixins/OxyServices.chains.js +70 -0
- package/dist/esm/mixins/OxyServices.store.js +263 -0
- package/dist/esm/mixins/OxyServices.utility.js +159 -104
- package/dist/esm/mixins/index.js +7 -0
- package/dist/esm/server/rateLimit.js +15 -6
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +7 -0
- package/dist/types/mixins/OxyServices.chains.d.ts +156 -0
- package/dist/types/mixins/OxyServices.store.d.ts +334 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +31 -8
- package/dist/types/mixins/index.d.ts +3 -1
- package/package.json +1 -1
- package/src/index.ts +30 -0
- package/src/mixins/OxyServices.chains.ts +134 -0
- package/src/mixins/OxyServices.store.ts +585 -0
- package/src/mixins/OxyServices.utility.ts +161 -108
- package/src/mixins/__tests__/chains.test.ts +113 -0
- package/src/mixins/__tests__/store.test.ts +304 -0
- package/src/mixins/__tests__/userTokenAuth.test.ts +746 -0
- package/src/mixins/index.ts +9 -0
- package/src/server/__tests__/rateLimit.test.ts +47 -0
- package/src/server/rateLimit.ts +18 -8
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chains — the shared record log every Oxy app reads and writes.
|
|
3
|
+
*
|
|
4
|
+
* A person has ONE chain. An app appends its own records to it and projects its
|
|
5
|
+
* feeds from what it reads back, instead of keeping a private copy of the same
|
|
6
|
+
* person's activity. This mixin is the client half of `/chains` in oxy-api, and
|
|
7
|
+
* it exists so that adopting the chain costs an app no HTTP of its own — the
|
|
8
|
+
* whole point of the shared substrate is that the second app writes less code
|
|
9
|
+
* than the first, not the same amount in a different file.
|
|
10
|
+
*
|
|
11
|
+
* ## Both calls are SERVICE-authenticated
|
|
12
|
+
*
|
|
13
|
+
* They go through `makeServiceRequest`, so they only work on a backend that has
|
|
14
|
+
* called `configureServiceAuth()`. That is not an accident of implementation: an
|
|
15
|
+
* append writes to someone else's chain and a read spans many subjects, so
|
|
16
|
+
* neither belongs in a browser holding a user session. A frontend that needs
|
|
17
|
+
* this asks its own backend.
|
|
18
|
+
*
|
|
19
|
+
* The authority is checked server-side and cannot be talked out of from here:
|
|
20
|
+
* `chains:write` plus the application's own `chainNamespaces` for an append,
|
|
21
|
+
* `chains:read` plus the public-collection policy for a read. A call that
|
|
22
|
+
* violates either gets a 403 or an empty page — this client adds no
|
|
23
|
+
* pre-validation that could drift from the server's answer.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { OxyServicesBase } from '../OxyServices.base';
|
|
27
|
+
|
|
28
|
+
/** A signed record as it comes back from a read. */
|
|
29
|
+
export interface ChainRecord<TRecord = Record<string, unknown>> {
|
|
30
|
+
recordId: string;
|
|
31
|
+
/** The subject whose chain it is — the person the record is about. */
|
|
32
|
+
oxyUserId: string;
|
|
33
|
+
/** The lexicon NSID, e.g. `app.mention.feed.post`. */
|
|
34
|
+
collection: string;
|
|
35
|
+
envelope: {
|
|
36
|
+
version: number;
|
|
37
|
+
type: string;
|
|
38
|
+
subject: string;
|
|
39
|
+
issuer: string;
|
|
40
|
+
record: TRecord;
|
|
41
|
+
issuedAt: number;
|
|
42
|
+
seq?: number;
|
|
43
|
+
prev?: string | null;
|
|
44
|
+
collection?: string;
|
|
45
|
+
rkey?: string;
|
|
46
|
+
publicKey: string;
|
|
47
|
+
alg: string;
|
|
48
|
+
signature: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One page of a multi-subject read. */
|
|
53
|
+
export interface ChainRecordPage<TRecord = Record<string, unknown>> {
|
|
54
|
+
records: ChainRecord<TRecord>[];
|
|
55
|
+
/**
|
|
56
|
+
* Opaque. Hand it back as `since` to continue; `null` at the end of the
|
|
57
|
+
* stream as of this snapshot. Never construct one.
|
|
58
|
+
*/
|
|
59
|
+
nextCursor: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** What an append returns once the record is on the chain. */
|
|
63
|
+
export interface AppendedChainRecord {
|
|
64
|
+
recordId: string;
|
|
65
|
+
seq: number;
|
|
66
|
+
envelope: ChainRecord['envelope'];
|
|
67
|
+
verified: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function OxyServicesChainsMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
71
|
+
return class extends Base {
|
|
72
|
+
constructor(...args: any[]) {
|
|
73
|
+
super(...(args as [any]));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Service-token request, implemented by the auth mixin earlier in the pipeline. */
|
|
77
|
+
declare makeServiceRequest: <R = unknown>(
|
|
78
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
79
|
+
url: string,
|
|
80
|
+
data?: unknown,
|
|
81
|
+
userId?: string,
|
|
82
|
+
) => Promise<R>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Append a record to `oxyUserId`'s chain under `collection`/`rkey`.
|
|
86
|
+
*
|
|
87
|
+
* Oxy issues and signs it; the calling app never holds a chain signing key.
|
|
88
|
+
* `rkey` is the app's own id for the thing — reusing it later supersedes the
|
|
89
|
+
* earlier record for that key, which is how an edit works.
|
|
90
|
+
*
|
|
91
|
+
* Requires the `chains:write` scope AND `collection` falling under one of
|
|
92
|
+
* this application's granted `chainNamespaces`. Both are enforced by the
|
|
93
|
+
* server; a violation throws with a 403.
|
|
94
|
+
*/
|
|
95
|
+
async appendChainRecord(params: {
|
|
96
|
+
oxyUserId: string;
|
|
97
|
+
collection: string;
|
|
98
|
+
rkey: string;
|
|
99
|
+
record: Record<string, unknown>;
|
|
100
|
+
}): Promise<AppendedChainRecord> {
|
|
101
|
+
return this.makeServiceRequest<AppendedChainRecord>('POST', '/chains/records', params);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Records published by any of `oxyUserIds` under any of `collections`,
|
|
106
|
+
* oldest first — the read a cross-app feed is projected from.
|
|
107
|
+
*
|
|
108
|
+
* Only collections Oxy declares PUBLIC come back, whatever is asked for; a
|
|
109
|
+
* private one yields nothing rather than an error.
|
|
110
|
+
*
|
|
111
|
+
* **Re-poll from slightly BEFORE your last cursor and dedupe by
|
|
112
|
+
* `recordId`.** The chain's pagination axis is a transaction-start
|
|
113
|
+
* timestamp, so a record can commit behind a cursor that already passed it.
|
|
114
|
+
* Re-delivering one costs bytes; skipping one costs a record that never
|
|
115
|
+
* appears. Projections are expected to be idempotent for exactly this
|
|
116
|
+
* reason.
|
|
117
|
+
*/
|
|
118
|
+
async readChainRecords<TRecord = Record<string, unknown>>(params: {
|
|
119
|
+
oxyUserIds: readonly string[];
|
|
120
|
+
collections: readonly string[];
|
|
121
|
+
since?: string | null;
|
|
122
|
+
limit?: number;
|
|
123
|
+
}): Promise<ChainRecordPage<TRecord>> {
|
|
124
|
+
const query = new URLSearchParams({
|
|
125
|
+
authors: params.oxyUserIds.join(','),
|
|
126
|
+
collections: params.collections.join(','),
|
|
127
|
+
});
|
|
128
|
+
if (params.since) query.set('since', params.since);
|
|
129
|
+
if (params.limit !== undefined) query.set('limit', String(params.limit));
|
|
130
|
+
|
|
131
|
+
return this.makeServiceRequest<ChainRecordPage<TRecord>>('GET', `/chains/records?${query.toString()}`);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|