@oxyhq/contracts 0.23.0 → 0.25.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.
@@ -0,0 +1,150 @@
1
+ /**
2
+ * The follow graph wire contract (`/v2/follows`).
3
+ *
4
+ * These types are the boundary between the API that owns the graph and every
5
+ * application that reads it. They live here — not in the API and not in the
6
+ * SDK — because both ends have to agree, and a shape defined on one side is a
7
+ * shape the other side re-declares slightly differently within a release or two.
8
+ *
9
+ * ## Why the state is three fields and not a boolean
10
+ *
11
+ * A user can follow something globally and turn it off in ONE application. That
12
+ * is a state the user themselves created, so the client has to be able to see
13
+ * it and say so — "following, but not shown here" is a sentence a boolean
14
+ * cannot express. `globalState`, `applicationMode` and `effectiveState` are
15
+ * therefore reported separately, and only the last one answers "does this
16
+ * appear in my feed right now".
17
+ *
18
+ * ## Why kinds are strings
19
+ *
20
+ * `FollowTargetKind` is a plain `string`, not a union. Applications register
21
+ * their own kinds at runtime (`mercaria.store`, `syra.artist`), so a union here
22
+ * would mean every new application in the ecosystem needs a release of this
23
+ * package before it can follow anything. The namespace rule is enforced by the
24
+ * database, which is the one place that can enforce it for applications this
25
+ * package has never heard of.
26
+ */
27
+ /**
28
+ * A registered target kind, always `<namespace>.<thing>`.
29
+ *
30
+ * The namespace is the owning application's, so two applications cannot define
31
+ * or silently redefine each other's kinds.
32
+ */
33
+ export type FollowTargetKind = string;
34
+ /**
35
+ * Where a relationship stands globally — the user's own decision, independent
36
+ * of any application. Mirrors the database's own enum, which is the authority.
37
+ *
38
+ * `requested` is a real state and not a transient one: a private account has to
39
+ * accept, and until it does the user has asked and is waiting. A client that
40
+ * renders it as "not following" invites a second request that changes nothing.
41
+ */
42
+ export type FollowState = 'none' | 'requested' | 'active' | 'rejected';
43
+ /**
44
+ * What this application should DO right now — the field a button renders.
45
+ *
46
+ * Note that "never followed" and "following, but switched off here" both come
47
+ * back as `not_following`, because the answer to "does this appear in my feed"
48
+ * is the same for both. They are still distinguishable, and a UI explaining
49
+ * itself must distinguish them: it is `globalState === 'active'` with
50
+ * `applicationMode === 'disabled'`.
51
+ */
52
+ export type FollowEffectiveState = 'not_following' | 'requested' | 'following';
53
+ /**
54
+ * What ONE application does with a relationship.
55
+ *
56
+ * `inherit` is the default and means "whatever the user decided globally".
57
+ * `disabled` is the interesting one: the user still follows, this application
58
+ * just does not act on it — which is what makes "follow everywhere, mute here"
59
+ * possible without the user losing the follow.
60
+ */
61
+ export type FollowApplicationMode = 'inherit' | 'enabled' | 'disabled';
62
+ /** A thing that can be followed. */
63
+ export interface FollowTarget {
64
+ id: string;
65
+ /**
66
+ * The stable, global identity of the thing — an Oxy URI for local objects, an
67
+ * ActivityPub actor URI for remote ones. What makes "the same target" the
68
+ * same across applications and across servers.
69
+ */
70
+ uri: string;
71
+ kind: FollowTargetKind;
72
+ /**
73
+ * A cached display snapshot (name, handle, avatar). Present so a follow list
74
+ * can render without one lookup per row; never authoritative — the owning
75
+ * application always holds the current version.
76
+ */
77
+ metadata?: Record<string, unknown>;
78
+ }
79
+ /** One row of the user's central follow list. */
80
+ export interface FollowRecord {
81
+ relationshipId: string;
82
+ target: FollowTarget;
83
+ globalState: FollowState;
84
+ applicationMode: FollowApplicationMode;
85
+ /**
86
+ * Whether this follow acts in the requesting application right now — the
87
+ * same field a button renders. List rows carry it so a follow list can seed
88
+ * `FollowTargetButton` without recomputing the server's derivation.
89
+ */
90
+ effectiveState: FollowEffectiveState;
91
+ /**
92
+ * Where the user was when they followed. Provenance for the audit trail and
93
+ * for notification routing — never authority: this application cannot undo
94
+ * what another one recorded.
95
+ */
96
+ originApplicationId: string | null;
97
+ /** Set only on a timed follow. ISO-8601. */
98
+ expiresAt?: string;
99
+ createdAt: string;
100
+ }
101
+ /**
102
+ * The three-part answer to "am I following this".
103
+ *
104
+ * `effectiveState` is what a button renders. The other two are what an
105
+ * explanation renders, and a client that shows a disabled follow as "not
106
+ * following" will be asked why the button does nothing.
107
+ */
108
+ export interface FollowStatus {
109
+ /** Absent when nothing has ever been followed. Every other operation needs it. */
110
+ relationshipId?: string;
111
+ globalState: FollowState;
112
+ applicationMode: FollowApplicationMode;
113
+ /** `following` only when followed globally AND not disabled here. */
114
+ effectiveState: FollowEffectiveState;
115
+ expiresAt?: string;
116
+ }
117
+ /**
118
+ * `PUT /v2/follows/:targetId` — `created: false` means it already existed.
119
+ *
120
+ * Carries the whole resulting status rather than a couple of fields off it, so
121
+ * a client can store the answer instead of reconstructing one. Reconstructing
122
+ * is where an optimistic update and the settled value drift: the derivation of
123
+ * `effectiveState` lives on the server, and a client recomputing it is a second
124
+ * implementation of a rule that has one.
125
+ */
126
+ export interface FollowMutation {
127
+ relationshipId: string;
128
+ created: boolean;
129
+ status: FollowStatus;
130
+ }
131
+ /** `DELETE /v2/follows/:relationshipId` — `removed: false` means it was already gone. */
132
+ export interface UnfollowMutation {
133
+ removed: boolean;
134
+ }
135
+ /** `GET /v2/me/follows` */
136
+ export interface FollowListPage {
137
+ follows: FollowRecord[];
138
+ /** Absent when the last page has been reached. */
139
+ nextCursor?: string;
140
+ }
141
+ /**
142
+ * Options for `PUT /v2/follows/:targetId`.
143
+ *
144
+ * `expiresIn` is the timed follow — seconds from now. Bounded server-side,
145
+ * because an unbounded value is indistinguishable from a permanent follow the
146
+ * user believes will end.
147
+ */
148
+ export interface FollowOptions {
149
+ expiresIn?: number;
150
+ }
@@ -37,6 +37,7 @@ export { MODERATION_SEVERITIES, MODERATION_FINDING_SCOPES, MODERATION_ATTRIBUTIO
37
37
  export type { ModerationSeverity, ModerationFindingScope, ModerationAttribution, ModerationDecisionStatus, ModerationEffectType, ModerationEffectStatus, ModerationEffectSkipReason, ConductStrikeStatus, ConductStanding, ContributionTier, PersonhoodStatusValue, IdentityBindingType, IdentityBindingStatus, ApplicationModerationStanding, ModerationFinding, ModerationDecisionEventSubject, ModerationPolicyVersions, ModerationDecisionEvent, FinalizeModerationDecisionInput, ReverseModerationEffectInput, ModerationEffect, ApplyModerationDecisionResult, ReverseModerationEffectResult, RegisterIdentityBindingInput, IdentityBinding, ReputationPersonhood, ReputationContribution, ReputationConduct, ReputationReporting, ReputationReviewing, ReputationContextualInfluence, ApplicationModerationTrust, } from './moderationReputation';
38
38
  export { linkPreviewSchema, linkPreviewBatchRequestSchema, linkPreviewBatchResponseSchema, linkPreviewResponseSchema, } from './links';
39
39
  export type { LinkPreviewStatus, LinkPreview, LinkPreviewBatchRequest, LinkPreviewBatchResponse, } from './links';
40
+ export type { FollowTargetKind, FollowState, FollowEffectiveState, FollowApplicationMode, FollowTarget, FollowRecord, FollowStatus, FollowMutation, UnfollowMutation, FollowListPage, FollowOptions, } from './followGraph';
40
41
  export { sessionAccountSchema, deviceSessionStateSchema, activeTokenSchema, deviceSessionSyncSchema, deviceTokenMintRequestSchema, deviceTokenMintResponseSchema, deviceBackgroundCredentialResponseSchema, deviceBackgroundTokenRequestSchema, deviceBackgroundTokenResponseSchema, SESSION_ACCOUNTS_CHANGED_EVENT, sessionAccountsChangedReasonSchema, sessionAccountsChangedEventSchema, } from './deviceSession';
41
42
  export type { SessionAccount, DeviceSessionState, ActiveToken, DeviceSessionSync, DeviceTokenMintRequest, DeviceTokenMintResponse, DeviceBackgroundCredentialResponse, DeviceBackgroundTokenRequest, DeviceBackgroundTokenResponse, SessionAccountsChangedReason, SessionAccountsChangedEvent, } from './deviceSession';
42
43
  export { loginResultSchema, } from './deviceBoot';
@@ -1,31 +1,59 @@
1
1
  /**
2
2
  * Oxy-scoped signed-record types.
3
3
  *
4
- * The base `signedRecordEnvelopeSchema` (`./identity`) now treats `type` as an
5
- * OPEN, non-empty string so ANY Oxy app may define its own record categories
6
- * (e.g. `app.mention.*`'s `app_record`) on the shared envelope grammar. The Oxy
7
- * identity/civic/node STORE, however, accepts ONLY the closed set of categories
8
- * it knows how to verify and materialize — this module is that closed set.
4
+ * The base `signedRecordEnvelopeSchema` (`./identity`) treats `type` as an OPEN,
5
+ * non-empty string so ANY Oxy app may sign on the shared envelope grammar. The
6
+ * Oxy STORE re-narrows it to the closed set in this module — a `type` outside it
7
+ * is rejected as `invalid_envelope`.
9
8
  *
10
- * `oxySignedRecordTypeSchema` is the runtime gate the Oxy store re-narrows with
11
- * (the API's `verifyEnvelope` rejects any `type` outside it; the Mongoose
12
- * `SignedRecord.type` enum is derived from `.options`); `OxySignedRecordType` is
13
- * the matching compile-time union the SDK identity/civic mixins type against.
9
+ * `oxySignedRecordTypeSchema` is that runtime gate (the API's `verifyEnvelope`
10
+ * re-narrows with it; the Mongoose `SignedRecord.type` enum and the Postgres
11
+ * CHECK on `signed_records.type` are both derived from `.options`);
12
+ * `OxySignedRecordType` is the matching compile-time union the SDK
13
+ * identity/civic mixins type against.
14
14
  *
15
15
  * The signing input INCLUDES `type`, so this set is part of the signed bytes —
16
- * a record cannot have its category swapped after signing.
16
+ * a record cannot have its category swapped after signing, and a value once
17
+ * signed can never be renamed.
17
18
  *
18
19
  * v1 only ever carried `identity` / `profile` (already in production); v2 added
19
20
  * the civic record types (reputation attestations, real-life / peer validations,
20
21
  * personhood vouches, verifiable credentials) and the user-node registration
21
- * record. Every value here is an Oxy `app.oxy.*` (or legacy v1) category — an
22
- * app's own `type` (e.g. `app_record`) is intentionally NOT in this set and is
23
- * rejected by the Oxy store.
22
+ * record.
23
+ *
24
+ * ## Why `app_record` is here, when it deliberately was not
25
+ *
26
+ * This set used to hold Oxy's own categories only, and said so: an app's `type`
27
+ * was "intentionally NOT in this set". The reason given was that the store
28
+ * accepts only what it knows how to **verify and materialize**. Verification
29
+ * turned out not to argue for the exclusion — the engine verifies a signature
30
+ * against the subject's keys whatever the category says — and materialization
31
+ * is the app's job, not the store's: an app projects its own feed tables from
32
+ * records it reads back.
33
+ *
34
+ * What changed is the decision the exclusion blocked. One chain per PERSON, held
35
+ * by Oxy, is the ecosystem substrate: apps append their records to the subject's
36
+ * one chain instead of each keeping a private chain for the same person. A
37
+ * closed set that admits no app category makes that unrepresentable.
38
+ *
39
+ * `app_record` is ONE value rather than an open lane, and the lexicon lives in
40
+ * the envelope's `collection` (`app.mention.feed.post`, `app.syra.*`), which the
41
+ * store denormalizes to `signed_records.nsid` and indexes. So a new app needs no
42
+ * change here — it picks its own collection namespace and signs `app_record`,
43
+ * exactly as Mention already does in production. Keeping the set closed is what
44
+ * keeps the CHECK a real constraint.
45
+ *
46
+ * **Admitting the category is not the whole of that decision.** Two gates sit
47
+ * beside it and are unchanged: an app record must arrive as a v2 (chained)
48
+ * envelope, and `oxyVerificationResolver` accepts exactly one custodial issuer
49
+ * (`OXY_DID`). So a record a user signs themselves verifies here today, while
50
+ * one an app signs custodially under its OWN issuer DID does not — that needs a
51
+ * separate, deliberate answer about which issuers may write to a person's chain.
24
52
  *
25
53
  * Platform-agnostic — zod only, no react/react-native/expo, ESM-safe.
26
54
  */
27
55
  import { z } from 'zod';
28
- export declare const oxySignedRecordTypeSchema: z.ZodEnum<["identity", "profile", "reputation_attestation", "real_life_attestation", "validation_verdict", "personhood_vouch", "credential", "node"]>;
56
+ export declare const oxySignedRecordTypeSchema: z.ZodEnum<["identity", "profile", "reputation_attestation", "real_life_attestation", "validation_verdict", "personhood_vouch", "credential", "node", "app_record"]>;
29
57
  /**
30
58
  * The closed set of record categories the Oxy identity/civic/node store accepts.
31
59
  * The base envelope `type` is an open string; this is what the Oxy store
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/contracts",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "OxyHQ API contracts — single source of truth for request/response Zod schemas and inferred types, shared by the backend and the client SDKs",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -26,6 +26,7 @@
26
26
  "./package.json": "./package.json"
27
27
  },
28
28
  "files": [
29
+ "NOTICE",
29
30
  "dist"
30
31
  ],
31
32
  "keywords": [
@@ -42,7 +43,7 @@
42
43
  "directory": "packages/contracts"
43
44
  },
44
45
  "author": "OxyHQ",
45
- "license": "AGPL-3.0-only",
46
+ "license": "Apache-2.0",
46
47
  "homepage": "https://oxy.so",
47
48
  "engines": {
48
49
  "node": ">=18.0.0"