@authzflow/server 0.1.0-alpha.8 → 0.1.0-beta.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @authzflow/server
2
2
 
3
- MIT-licensed alpha complete-check provider maintained by Accelflow Inc. Ships ESM JavaScript and TypeScript declarations, with no runtime dependencies or Rust build required.
3
+ MIT-licensed beta complete-check provider maintained by Accelflow Inc. Ships ESM JavaScript and TypeScript declarations, with no runtime dependencies or Rust build required.
4
4
 
5
5
  ```js
6
6
  import { createCheckProvider } from '@authzflow/server';
@@ -10,6 +10,13 @@ Connect existing authentication and authorization with the async `authenticate`
10
10
 
11
11
  See https://github.com/Accelflow/AuthzFlow/blob/main/docs-public/adapters.md and https://github.com/Accelflow/AuthzFlow/blob/main/docs-public/protocol.md for callback and wire contracts. Node 22/24 and TypeScript 5.9+ are initial targets. Native Rust servers can use the companion authzflow-server source crate with the same wire fixtures.
12
12
 
13
- The checked reader in @authzflow/core validates these responses before returning content. Always keep authorization on ordinary product content APIs. Reliable event streams and cursor recovery are outside this complete-check alpha.
13
+ The checked reader in @authzflow/core validates these responses before returning content. Always keep authorization on ordinary product content APIs. Reliable event streams and cursor recovery are outside this complete-check beta.
14
14
 
15
15
  LICENSE and THIRD_PARTY_NOTICES.md are included. Security reporting: https://github.com/Accelflow/AuthzFlow/security/advisories/new .
16
+
17
+ `createResourceGroupFeed` is the opt-in metadata-only protocol v2 feed.
18
+ `createResourceGroupCheck` attaches the server-selected Group watermark to an
19
+ existing successful Resource check inside the host journal barrier. Products
20
+ retain ACL decisions, transactional journal durability, cursor authentication,
21
+ former-subscriber entitlement, and retention. The feed discloses only Group-wide
22
+ revoke/revalidate events. It does not grant article access or replace existing v1.
package/dist/index.d.ts CHANGED
@@ -68,3 +68,4 @@ export type CheckProvider<Context, Target> = {
68
68
  }): Promise<CheckResult>;
69
69
  };
70
70
  export declare function createCheckProvider<Context, Principal, Target>(options: ProviderOptions<Context, Principal, Target>): CheckProvider<Context, Target>;
71
+ export { createResourceGroupFeed, createResourceGroupCheck, type ResourceGroupJournal, type ResourceGroupTarget, type ResourceGroupPosition, type ResourceGroupFeedOptions } from './resource-group-feed.js';
package/dist/index.js CHANGED
@@ -58,3 +58,4 @@ export function createCheckProvider(options) {
58
58
  },
59
59
  };
60
60
  }
61
+ export { createResourceGroupFeed, createResourceGroupCheck } from './resource-group-feed.js';
@@ -0,0 +1,75 @@
1
+ export interface ResourceGroupTarget {
2
+ resourceGroupId: string;
3
+ }
4
+ export interface ResourceGroupPosition {
5
+ epoch: string;
6
+ sequence: string;
7
+ cursor: string;
8
+ }
9
+ export interface ResourceGroupJournal<Target extends ResourceGroupTarget> {
10
+ barrier<T>(work: () => Promise<T>): Promise<T>;
11
+ /** Authoritative entitlement, including a bounded former-subscriber revocation path. */
12
+ subscribe(binding: string, target: Target): Promise<ResourceGroupPosition>;
13
+ read(binding: string, target: Target, cursor: string, limit: number): Promise<{
14
+ batch: {
15
+ binding: string;
16
+ resourceGroupId: string;
17
+ epoch: string;
18
+ fromSequence: string;
19
+ throughSequence: string;
20
+ nextCursor: string;
21
+ changes: {
22
+ sequence: string;
23
+ action: 'revoke' | 'revalidate';
24
+ }[];
25
+ };
26
+ hasMore: boolean;
27
+ }>;
28
+ }
29
+ type FeedResult = {
30
+ status: number;
31
+ headers: Readonly<{
32
+ 'Cache-Control': 'no-store';
33
+ }>;
34
+ body: unknown;
35
+ };
36
+ export interface ResourceGroupFeedOptions<Context, Target extends ResourceGroupTarget> {
37
+ authenticate(context: Context): Promise<{
38
+ binding: string;
39
+ } | null>;
40
+ journal: ResourceGroupJournal<Target>;
41
+ }
42
+ /** Metadata-only Group feed. Product journal must enforce cursor and subscriber binding. */
43
+ export declare function createResourceGroupFeed<Context, Target extends ResourceGroupTarget>(options: ResourceGroupFeedOptions<Context, Target>): {
44
+ subscribe({ query: q, context, target }: {
45
+ query: Readonly<Record<string, unknown>>;
46
+ context: Context;
47
+ target: Target;
48
+ }): Promise<FeedResult>;
49
+ changes({ query: q, context, target }: {
50
+ query: Readonly<Record<string, unknown>>;
51
+ context: Context;
52
+ target: Target;
53
+ }): Promise<FeedResult>;
54
+ };
55
+ /** Attach a trusted Group watermark inside the SAME ACL/journal barrier. No group-wide allow. */
56
+ export declare function createResourceGroupCheck<Context, Target>(options: {
57
+ check(input: {
58
+ query: Readonly<Record<string, unknown>>;
59
+ context: Context;
60
+ target: Target;
61
+ }): Promise<{
62
+ status: number;
63
+ headers: Readonly<{
64
+ 'Cache-Control': 'no-store';
65
+ }>;
66
+ body: unknown;
67
+ }>;
68
+ resolveGroup(target: Target): Promise<ResourceGroupTarget>;
69
+ journal: ResourceGroupJournal<ResourceGroupTarget>;
70
+ }): (input: {
71
+ query: Readonly<Record<string, unknown>>;
72
+ context: Context;
73
+ target: Target;
74
+ }) => Promise<FeedResult>;
75
+ export {};
@@ -0,0 +1,78 @@
1
+ /** Metadata-only Group feed. Product journal must enforce cursor and subscriber binding. */
2
+ export function createResourceGroupFeed(options) {
3
+ const headers = Object.freeze({ 'Cache-Control': 'no-store' });
4
+ const error = (status, code) => ({ status, headers, body: { error: { code } } });
5
+ const identity = async (context) => {
6
+ const actor = await options.authenticate(context);
7
+ if (actor && (typeof actor.binding !== 'string' || !actor.binding || actor.binding.length > 4096))
8
+ throw Error('group_identity_invalid');
9
+ return actor;
10
+ };
11
+ const query = (value, cursor) => value && !Array.isArray(value) && typeof value.requestId === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value.requestId) && Object.keys(value).length === (cursor ? 2 : 1) && (!cursor || typeof value.cursor === 'string' && value.cursor.length > 0 && value.cursor.length <= 8192);
12
+ return {
13
+ async subscribe({ query: q, context, target }) {
14
+ if (!query(q, false) || typeof target?.resourceGroupId !== 'string' || !target.resourceGroupId || target.resourceGroupId.length > 4096)
15
+ return error(400, 'invalid_group_request');
16
+ try {
17
+ const actor = await identity(context);
18
+ if (!actor)
19
+ return error(401, 'user_session_required');
20
+ const position = await options.journal.barrier(() => options.journal.subscribe(actor.binding, target));
21
+ if (!position || Object.keys(position).sort().join(',') !== 'cursor,epoch,sequence' || typeof position.epoch !== 'string' || !position.epoch || position.epoch.length > 128 || typeof position.cursor !== 'string' || !position.cursor || position.cursor.length > 8192 || typeof position.sequence !== 'string' || !/^(0|[1-9][0-9]*)$/.test(position.sequence) || position.sequence.length > 20 || BigInt(position.sequence) > 18446744073709551615n)
22
+ throw Error('group_position_invalid');
23
+ return { status: 200, headers, body: { schemaVersion: 2, kind: 'resource_group', requestId: q.requestId, binding: actor.binding, resourceGroupId: target.resourceGroupId, position } };
24
+ }
25
+ catch (failure) {
26
+ return failure instanceof Error && failure.message === 'access_required' ? error(403, 'access_required') : error(503, 'group_unavailable');
27
+ }
28
+ },
29
+ async changes({ query: q, context, target }) {
30
+ if (!query(q, true) || typeof target?.resourceGroupId !== 'string' || !target.resourceGroupId || target.resourceGroupId.length > 4096)
31
+ return error(400, 'invalid_group_request');
32
+ try {
33
+ const actor = await identity(context);
34
+ if (!actor)
35
+ return error(401, 'user_session_required');
36
+ const result = await options.journal.barrier(() => options.journal.read(actor.binding, target, q.cursor, 100));
37
+ // The producer also rejects Resource IDs/unknown event fields, not just the consumer.
38
+ const b = result.batch;
39
+ if (!b || Object.keys(b).sort().join(',') !== 'binding,changes,epoch,fromSequence,nextCursor,resourceGroupId,throughSequence' || typeof result.hasMore !== 'boolean' || b.binding !== actor.binding || b.resourceGroupId !== target.resourceGroupId || typeof b.epoch !== 'string' || !b.epoch || b.epoch.length > 128 || typeof b.nextCursor !== 'string' || !b.nextCursor || b.nextCursor.length > 8192 || !Array.isArray(b.changes) || b.changes.length > 100 || b.changes.some(e => Object.keys(e).sort().join(',') !== 'action,sequence' || !['revoke', 'revalidate'].includes(e.action)))
40
+ throw Error('group_batch_invalid');
41
+ let previous = BigInt(b.fromSequence);
42
+ if (!/^(0|[1-9][0-9]*)$/.test(b.fromSequence) || !/^(0|[1-9][0-9]*)$/.test(b.throughSequence))
43
+ throw Error('group_batch_invalid');
44
+ for (const e of b.changes) {
45
+ previous++;
46
+ if (String(previous) !== e.sequence)
47
+ throw Error('group_batch_invalid');
48
+ }
49
+ if (String(previous) !== b.throughSequence || previous > 18446744073709551615n)
50
+ throw Error('group_batch_invalid');
51
+ return { status: 200, headers, body: { schemaVersion: 2, kind: 'resource_group', requestId: q.requestId, batch: b, hasMore: result.hasMore } };
52
+ }
53
+ catch (failure) {
54
+ return failure instanceof Error && failure.message === 'cursor_reset_required' ? error(409, 'reset_required') : error(503, 'group_unavailable');
55
+ }
56
+ },
57
+ };
58
+ }
59
+ /** Attach a trusted Group watermark inside the SAME ACL/journal barrier. No group-wide allow. */
60
+ export function createResourceGroupCheck(options) {
61
+ return async (input) => {
62
+ try {
63
+ return await options.journal.barrier(async () => {
64
+ const result = await options.check(input);
65
+ if (result.status !== 200)
66
+ return result;
67
+ const body = result.body;
68
+ if (typeof body?.binding !== 'string')
69
+ throw Error('group_identity_invalid');
70
+ const target = await options.resolveGroup(input.target), position = await options.journal.subscribe(body.binding, target);
71
+ return { ...result, headers: { 'Cache-Control': 'no-store' }, body: { ...body, resourceGroup: { resourceGroupId: target.resourceGroupId, epoch: position.epoch, sequence: position.sequence } } };
72
+ });
73
+ }
74
+ catch {
75
+ return { status: 503, headers: { 'Cache-Control': 'no-store' }, body: { error: { code: 'group_check_unavailable' } } };
76
+ }
77
+ };
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authzflow/server",
3
- "version": "0.1.0-alpha.8",
3
+ "version": "0.1.0-beta.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "author": "Accelflow Inc.",