@slates/provider-handler 1.0.0-rc.40 → 1.0.0-rc.42

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/index.cjs CHANGED
@@ -124,7 +124,8 @@ var mapAuthMethod = (slate, m) => ({
124
124
  handleChangedInput: {
125
125
  enabled: !!m.onInputChanged
126
126
  },
127
- getProfile: { enabled: !!m.getProfile }
127
+ getProfile: { enabled: !!m.getProfile },
128
+ ..."adminSetup" in m && m.adminSetup ? { adminSetup: { enabled: true, required: m.adminSetup.required === true } } : {}
128
129
  },
129
130
  docs: m.docs ?? []
130
131
  });
@@ -548,6 +549,57 @@ var createProviderHandler = (slate, listeners) => (0, import_proto.createSlatesP
548
549
  let requestTraces = context.getHttpTraces();
549
550
  return requestTraces.length > 0 ? { ...result, requestTraces } : result;
550
551
  };
552
+ let buildAuthorizationUrlContext = (params) => ({
553
+ redirectUri: params.redirectUri,
554
+ state: params.state,
555
+ input: params.input,
556
+ clientId: params.clientId,
557
+ clientSecret: params.clientSecret,
558
+ scopes: params.scopes,
559
+ config: getAuthConfig()
560
+ });
561
+ let buildCallbackContext = (params) => ({
562
+ state: params.state,
563
+ redirectUri: params.redirectUri,
564
+ input: params.input,
565
+ clientId: params.clientId,
566
+ clientSecret: params.clientSecret,
567
+ scopes: params.scopes,
568
+ callbackParams: params.callbackParams || {},
569
+ callbackState: params.callbackState || {},
570
+ config: getAuthConfig()
571
+ });
572
+ let getTracedAuthorizationUrl = async (params, authenticationMethodName, trace, getAuthorizationUrl) => {
573
+ let context = getEmptyContext();
574
+ let urlRes = await traceProviderCall(
575
+ {
576
+ component: "auth",
577
+ functionName: trace.functionName,
578
+ message: trace.message,
579
+ successMessage: trace.successMessage,
580
+ metadata: {
581
+ authenticationMethodId: params.authenticationMethodId,
582
+ authenticationMethodName,
583
+ scopeCount: params.scopes.length,
584
+ inputKeyCount: getObjectKeyCount(params.input),
585
+ ...trace.metadata
586
+ },
587
+ onSuccess: (result) => ({
588
+ returnedInput: !!result.input,
589
+ hasCallbackState: !!result.callbackState
590
+ })
591
+ },
592
+ () => (0, import_provider2.runWithContext)(
593
+ context,
594
+ () => getAuthorizationUrl(buildAuthorizationUrlContext(params))
595
+ )
596
+ );
597
+ return withRequestTraces(context, {
598
+ authorizationUrl: urlRes.url,
599
+ input: urlRes.input,
600
+ callbackState: urlRes.callbackState
601
+ });
602
+ };
551
603
  manager.onNotification("slates/hello", async ({ params }) => {
552
604
  protocol.set(params.protocol);
553
605
  });
@@ -839,18 +891,7 @@ var createProviderHandler = (slate, listeners) => (0, import_proto.createSlatesP
839
891
  },
840
892
  () => (0, import_provider2.runWithContext)(
841
893
  context,
842
- () => authMethod.handleCallback({
843
- code: params.code,
844
- state: params.state,
845
- redirectUri: params.redirectUri,
846
- input: params.input,
847
- clientId: params.clientId,
848
- clientSecret: params.clientSecret,
849
- scopes: params.scopes,
850
- callbackParams: params.callbackParams || {},
851
- callbackState: params.callbackState || {},
852
- config: getAuthConfig()
853
- })
894
+ () => authMethod.handleCallback({ ...buildCallbackContext(params), code: params.code })
854
895
  )
855
896
  );
856
897
  return withRequestTraces(context, {
@@ -869,46 +910,93 @@ var createProviderHandler = (slate, listeners) => (0, import_proto.createSlatesP
869
910
  getContextBasic();
870
911
  let authMethod = getAuthMethod(slate, params.authenticationMethodId);
871
912
  if ("getAuthorizationUrl" in authMethod) {
872
- let context = getEmptyContext();
873
- let urlRes = await traceProviderCall(
913
+ return getTracedAuthorizationUrl(
914
+ params,
915
+ authMethod.name,
874
916
  {
875
- component: "auth",
876
917
  functionName: "getAuthorizationUrl",
877
918
  message: "Getting authentication authorization URL",
878
- successMessage: "Authentication authorization URL retrieved",
919
+ successMessage: "Authentication authorization URL retrieved"
920
+ },
921
+ (ctx) => authMethod.getAuthorizationUrl(ctx)
922
+ );
923
+ }
924
+ throw new import_error4.ServiceError(
925
+ (0, import_error4.preconditionFailedError)({
926
+ message: `Authentication method does not support authorization URL retrieval: ${params.authenticationMethodId}`
927
+ })
928
+ );
929
+ });
930
+ manager.onRequest("slates/auth.admin_authorization_url.get", async ({ params }) => {
931
+ getContextBasic();
932
+ let authMethod = getAuthMethod(slate, params.authenticationMethodId);
933
+ if (authMethod.type === "auth.oauth" && authMethod.adminSetup) {
934
+ let { adminSetup } = authMethod;
935
+ return getTracedAuthorizationUrl(
936
+ params,
937
+ authMethod.name,
938
+ {
939
+ functionName: "adminSetup.getAuthorizationUrl",
940
+ message: "Getting admin setup authorization URL",
941
+ successMessage: "Admin setup authorization URL retrieved",
942
+ metadata: { usesDefault: !adminSetup.getAuthorizationUrl }
943
+ },
944
+ (ctx) => adminSetup.getAuthorizationUrl ? adminSetup.getAuthorizationUrl(ctx) : authMethod.getAuthorizationUrl(ctx)
945
+ );
946
+ }
947
+ throw new import_error4.ServiceError(
948
+ (0, import_error4.preconditionFailedError)({
949
+ message: `Authentication method does not support admin setup: ${params.authenticationMethodId}`
950
+ })
951
+ );
952
+ });
953
+ manager.onRequest("slates/auth.admin_callback.handle", async ({ params }) => {
954
+ getContextBasic();
955
+ let authMethod = getAuthMethod(slate, params.authenticationMethodId);
956
+ if (authMethod.type === "auth.oauth" && authMethod.adminSetup) {
957
+ let { adminSetup } = authMethod;
958
+ let ctx = { ...buildCallbackContext(params), code: params.code };
959
+ let handleCallback = async () => {
960
+ if (adminSetup.handleCallback) return adminSetup.handleCallback(ctx);
961
+ let code = ctx.code;
962
+ if (!code) {
963
+ throw new import_error4.ServiceError(
964
+ (0, import_error4.badRequestError)({
965
+ message: `Admin setup callback did not include an authorization code: ${params.authenticationMethodId}`
966
+ })
967
+ );
968
+ }
969
+ await authMethod.handleCallback({ ...ctx, code });
970
+ return {};
971
+ };
972
+ let context = getEmptyContext();
973
+ let callbackRes = await traceProviderCall(
974
+ {
975
+ component: "auth",
976
+ functionName: "adminSetup.handleCallback",
977
+ message: "Handling admin setup callback",
978
+ successMessage: "Admin setup callback handled",
879
979
  metadata: {
880
980
  authenticationMethodId: params.authenticationMethodId,
881
981
  authenticationMethodName: authMethod.name,
882
982
  scopeCount: params.scopes.length,
883
- inputKeyCount: getObjectKeyCount(params.input)
983
+ hasCode: !!params.code,
984
+ hasCallbackState: !!params.callbackState,
985
+ usesDefault: !adminSetup.handleCallback
884
986
  },
885
987
  onSuccess: (result) => ({
886
- returnedInput: !!result.input,
887
- hasCallbackState: !!result.callbackState
988
+ returnedInstructions: !!result.instructions
888
989
  })
889
990
  },
890
- () => (0, import_provider2.runWithContext)(
891
- context,
892
- () => authMethod.getAuthorizationUrl({
893
- redirectUri: params.redirectUri,
894
- state: params.state,
895
- input: params.input,
896
- clientId: params.clientId,
897
- clientSecret: params.clientSecret,
898
- scopes: params.scopes,
899
- config: getAuthConfig()
900
- })
901
- )
991
+ () => (0, import_provider2.runWithContext)(context, handleCallback)
902
992
  );
903
993
  return withRequestTraces(context, {
904
- authorizationUrl: urlRes.url,
905
- input: urlRes.input,
906
- callbackState: urlRes.callbackState
994
+ instructions: callbackRes.instructions
907
995
  });
908
996
  }
909
997
  throw new import_error4.ServiceError(
910
998
  (0, import_error4.preconditionFailedError)({
911
- message: `Authentication method does not support authorization URL retrieval: ${params.authenticationMethodId}`
999
+ message: `Authentication method does not support admin setup: ${params.authenticationMethodId}`
912
1000
  })
913
1001
  );
914
1002
  });
@@ -108,7 +108,8 @@ var mapAuthMethod = (slate, m) => ({
108
108
  handleChangedInput: {
109
109
  enabled: !!m.onInputChanged
110
110
  },
111
- getProfile: { enabled: !!m.getProfile }
111
+ getProfile: { enabled: !!m.getProfile },
112
+ ..."adminSetup" in m && m.adminSetup ? { adminSetup: { enabled: true, required: m.adminSetup.required === true } } : {}
112
113
  },
113
114
  docs: m.docs ?? []
114
115
  });
@@ -532,6 +533,57 @@ var createProviderHandler = (slate, listeners) => createSlatesProviderProtoHandl
532
533
  let requestTraces = context.getHttpTraces();
533
534
  return requestTraces.length > 0 ? { ...result, requestTraces } : result;
534
535
  };
536
+ let buildAuthorizationUrlContext = (params) => ({
537
+ redirectUri: params.redirectUri,
538
+ state: params.state,
539
+ input: params.input,
540
+ clientId: params.clientId,
541
+ clientSecret: params.clientSecret,
542
+ scopes: params.scopes,
543
+ config: getAuthConfig()
544
+ });
545
+ let buildCallbackContext = (params) => ({
546
+ state: params.state,
547
+ redirectUri: params.redirectUri,
548
+ input: params.input,
549
+ clientId: params.clientId,
550
+ clientSecret: params.clientSecret,
551
+ scopes: params.scopes,
552
+ callbackParams: params.callbackParams || {},
553
+ callbackState: params.callbackState || {},
554
+ config: getAuthConfig()
555
+ });
556
+ let getTracedAuthorizationUrl = async (params, authenticationMethodName, trace, getAuthorizationUrl) => {
557
+ let context = getEmptyContext();
558
+ let urlRes = await traceProviderCall(
559
+ {
560
+ component: "auth",
561
+ functionName: trace.functionName,
562
+ message: trace.message,
563
+ successMessage: trace.successMessage,
564
+ metadata: {
565
+ authenticationMethodId: params.authenticationMethodId,
566
+ authenticationMethodName,
567
+ scopeCount: params.scopes.length,
568
+ inputKeyCount: getObjectKeyCount(params.input),
569
+ ...trace.metadata
570
+ },
571
+ onSuccess: (result) => ({
572
+ returnedInput: !!result.input,
573
+ hasCallbackState: !!result.callbackState
574
+ })
575
+ },
576
+ () => runWithContext(
577
+ context,
578
+ () => getAuthorizationUrl(buildAuthorizationUrlContext(params))
579
+ )
580
+ );
581
+ return withRequestTraces(context, {
582
+ authorizationUrl: urlRes.url,
583
+ input: urlRes.input,
584
+ callbackState: urlRes.callbackState
585
+ });
586
+ };
535
587
  manager.onNotification("slates/hello", async ({ params }) => {
536
588
  protocol.set(params.protocol);
537
589
  });
@@ -823,18 +875,7 @@ var createProviderHandler = (slate, listeners) => createSlatesProviderProtoHandl
823
875
  },
824
876
  () => runWithContext(
825
877
  context,
826
- () => authMethod.handleCallback({
827
- code: params.code,
828
- state: params.state,
829
- redirectUri: params.redirectUri,
830
- input: params.input,
831
- clientId: params.clientId,
832
- clientSecret: params.clientSecret,
833
- scopes: params.scopes,
834
- callbackParams: params.callbackParams || {},
835
- callbackState: params.callbackState || {},
836
- config: getAuthConfig()
837
- })
878
+ () => authMethod.handleCallback({ ...buildCallbackContext(params), code: params.code })
838
879
  )
839
880
  );
840
881
  return withRequestTraces(context, {
@@ -853,46 +894,93 @@ var createProviderHandler = (slate, listeners) => createSlatesProviderProtoHandl
853
894
  getContextBasic();
854
895
  let authMethod = getAuthMethod(slate, params.authenticationMethodId);
855
896
  if ("getAuthorizationUrl" in authMethod) {
856
- let context = getEmptyContext();
857
- let urlRes = await traceProviderCall(
897
+ return getTracedAuthorizationUrl(
898
+ params,
899
+ authMethod.name,
858
900
  {
859
- component: "auth",
860
901
  functionName: "getAuthorizationUrl",
861
902
  message: "Getting authentication authorization URL",
862
- successMessage: "Authentication authorization URL retrieved",
903
+ successMessage: "Authentication authorization URL retrieved"
904
+ },
905
+ (ctx) => authMethod.getAuthorizationUrl(ctx)
906
+ );
907
+ }
908
+ throw new ServiceError4(
909
+ preconditionFailedError({
910
+ message: `Authentication method does not support authorization URL retrieval: ${params.authenticationMethodId}`
911
+ })
912
+ );
913
+ });
914
+ manager.onRequest("slates/auth.admin_authorization_url.get", async ({ params }) => {
915
+ getContextBasic();
916
+ let authMethod = getAuthMethod(slate, params.authenticationMethodId);
917
+ if (authMethod.type === "auth.oauth" && authMethod.adminSetup) {
918
+ let { adminSetup } = authMethod;
919
+ return getTracedAuthorizationUrl(
920
+ params,
921
+ authMethod.name,
922
+ {
923
+ functionName: "adminSetup.getAuthorizationUrl",
924
+ message: "Getting admin setup authorization URL",
925
+ successMessage: "Admin setup authorization URL retrieved",
926
+ metadata: { usesDefault: !adminSetup.getAuthorizationUrl }
927
+ },
928
+ (ctx) => adminSetup.getAuthorizationUrl ? adminSetup.getAuthorizationUrl(ctx) : authMethod.getAuthorizationUrl(ctx)
929
+ );
930
+ }
931
+ throw new ServiceError4(
932
+ preconditionFailedError({
933
+ message: `Authentication method does not support admin setup: ${params.authenticationMethodId}`
934
+ })
935
+ );
936
+ });
937
+ manager.onRequest("slates/auth.admin_callback.handle", async ({ params }) => {
938
+ getContextBasic();
939
+ let authMethod = getAuthMethod(slate, params.authenticationMethodId);
940
+ if (authMethod.type === "auth.oauth" && authMethod.adminSetup) {
941
+ let { adminSetup } = authMethod;
942
+ let ctx = { ...buildCallbackContext(params), code: params.code };
943
+ let handleCallback = async () => {
944
+ if (adminSetup.handleCallback) return adminSetup.handleCallback(ctx);
945
+ let code = ctx.code;
946
+ if (!code) {
947
+ throw new ServiceError4(
948
+ badRequestError3({
949
+ message: `Admin setup callback did not include an authorization code: ${params.authenticationMethodId}`
950
+ })
951
+ );
952
+ }
953
+ await authMethod.handleCallback({ ...ctx, code });
954
+ return {};
955
+ };
956
+ let context = getEmptyContext();
957
+ let callbackRes = await traceProviderCall(
958
+ {
959
+ component: "auth",
960
+ functionName: "adminSetup.handleCallback",
961
+ message: "Handling admin setup callback",
962
+ successMessage: "Admin setup callback handled",
863
963
  metadata: {
864
964
  authenticationMethodId: params.authenticationMethodId,
865
965
  authenticationMethodName: authMethod.name,
866
966
  scopeCount: params.scopes.length,
867
- inputKeyCount: getObjectKeyCount(params.input)
967
+ hasCode: !!params.code,
968
+ hasCallbackState: !!params.callbackState,
969
+ usesDefault: !adminSetup.handleCallback
868
970
  },
869
971
  onSuccess: (result) => ({
870
- returnedInput: !!result.input,
871
- hasCallbackState: !!result.callbackState
972
+ returnedInstructions: !!result.instructions
872
973
  })
873
974
  },
874
- () => runWithContext(
875
- context,
876
- () => authMethod.getAuthorizationUrl({
877
- redirectUri: params.redirectUri,
878
- state: params.state,
879
- input: params.input,
880
- clientId: params.clientId,
881
- clientSecret: params.clientSecret,
882
- scopes: params.scopes,
883
- config: getAuthConfig()
884
- })
885
- )
975
+ () => runWithContext(context, handleCallback)
886
976
  );
887
977
  return withRequestTraces(context, {
888
- authorizationUrl: urlRes.url,
889
- input: urlRes.input,
890
- callbackState: urlRes.callbackState
978
+ instructions: callbackRes.instructions
891
979
  });
892
980
  }
893
981
  throw new ServiceError4(
894
982
  preconditionFailedError({
895
- message: `Authentication method does not support authorization URL retrieval: ${params.authenticationMethodId}`
983
+ message: `Authentication method does not support admin setup: ${params.authenticationMethodId}`
896
984
  })
897
985
  );
898
986
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slates/provider-handler",
3
- "version": "1.0.0-rc.40",
3
+ "version": "1.0.0-rc.42",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@lowerdeck/error": "^1.1.0",
35
- "@slates/proto": "1.0.0-rc.25",
36
- "@slates/provider": "1.0.0-rc.30",
35
+ "@slates/proto": "1.0.0-rc.27",
36
+ "@slates/provider": "1.0.0-rc.32",
37
37
  "p-queue": "^6.6.2",
38
38
  "zod": "^4.2.1"
39
39
  },
@@ -0,0 +1,189 @@
1
+ import { SLATES_PROTOCOL_VERSION, SlatesProviderProtoHandlerManager } from '@slates/proto';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { z } from 'zod';
4
+ import { createProviderHandler } from './index';
5
+
6
+ let createManager = async (method: Record<string, any>) => {
7
+ let slate = {
8
+ spec: {
9
+ key: 'test',
10
+ name: 'Test',
11
+ auth: {
12
+ authStack: [
13
+ {
14
+ type: 'auth.oauth',
15
+ key: 'oauth',
16
+ name: 'OAuth',
17
+ scopes: [],
18
+ ...method
19
+ }
20
+ ],
21
+ outputSchema: z.object({ token: z.string() })
22
+ }
23
+ },
24
+ adapters: [],
25
+ actions: [],
26
+ triggerGroups: []
27
+ };
28
+ let manager = await createProviderHandler(slate as any, []).run();
29
+
30
+ await SlatesProviderProtoHandlerManager.handleInput(manager, {
31
+ jsonrpc: '2.0',
32
+ method: 'slates/hello',
33
+ params: { protocol: SLATES_PROTOCOL_VERSION }
34
+ });
35
+ await SlatesProviderProtoHandlerManager.handleInput(manager, {
36
+ jsonrpc: '2.0',
37
+ method: 'slates/participant.set',
38
+ params: { participants: [] }
39
+ });
40
+
41
+ return manager;
42
+ };
43
+
44
+ let baseParams = {
45
+ authenticationMethodId: 'oauth',
46
+ state: 'state-123',
47
+ redirectUri: 'https://hub.example/callback',
48
+ input: { tenant: 'acme' },
49
+ clientId: 'client-id',
50
+ clientSecret: 'client-secret',
51
+ scopes: ['read']
52
+ };
53
+
54
+ let getAdminAuthorizationUrl = (manager: SlatesProviderProtoHandlerManager) =>
55
+ SlatesProviderProtoHandlerManager.handleInput(manager, {
56
+ jsonrpc: '2.0',
57
+ id: 'url',
58
+ method: 'slates/auth.admin_authorization_url.get',
59
+ params: baseParams
60
+ });
61
+
62
+ let handleAdminCallback = (
63
+ manager: SlatesProviderProtoHandlerManager,
64
+ params: { code?: string; callbackParams?: Record<string, string> } = {}
65
+ ) =>
66
+ SlatesProviderProtoHandlerManager.handleInput(manager, {
67
+ jsonrpc: '2.0',
68
+ id: 'callback',
69
+ method: 'slates/auth.admin_callback.handle',
70
+ params: { ...baseParams, ...params }
71
+ });
72
+
73
+ let regularMethod = () => ({
74
+ getAuthorizationUrl: vi.fn(async () => ({
75
+ url: 'https://provider.example/authorize',
76
+ callbackState: { verifier: 'regular' }
77
+ })),
78
+ handleCallback: vi.fn(async () => ({ output: { token: 'user-token' } }))
79
+ });
80
+
81
+ describe('slates/auth.admin_authorization_url.get', () => {
82
+ it('falls back to the regular authorization URL', async () => {
83
+ let method = regularMethod();
84
+ let manager = await createManager({ ...method, adminSetup: {} });
85
+
86
+ expect(await getAdminAuthorizationUrl(manager)).toEqual({
87
+ jsonrpc: '2.0',
88
+ id: 'url',
89
+ result: {
90
+ authorizationUrl: 'https://provider.example/authorize',
91
+ callbackState: { verifier: 'regular' }
92
+ }
93
+ });
94
+ let { authenticationMethodId: _, ...ctx } = baseParams;
95
+ expect(method.getAuthorizationUrl).toHaveBeenCalledWith({ ...ctx, config: {} });
96
+ });
97
+
98
+ it('uses the admin authorization URL override', async () => {
99
+ let method = regularMethod();
100
+ let adminGetAuthorizationUrl = vi.fn(async () => ({
101
+ url: 'https://provider.example/adminconsent',
102
+ input: { tenant: 'acme' }
103
+ }));
104
+ let manager = await createManager({
105
+ ...method,
106
+ adminSetup: { getAuthorizationUrl: adminGetAuthorizationUrl }
107
+ });
108
+
109
+ expect(await getAdminAuthorizationUrl(manager)).toMatchObject({
110
+ result: {
111
+ authorizationUrl: 'https://provider.example/adminconsent',
112
+ input: { tenant: 'acme' }
113
+ }
114
+ });
115
+ expect(adminGetAuthorizationUrl).toHaveBeenCalledWith(
116
+ expect.objectContaining({ clientId: 'client-id', state: 'state-123', config: {} })
117
+ );
118
+ expect(method.getAuthorizationUrl).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it('rejects methods without admin setup', async () => {
122
+ let manager = await createManager(regularMethod());
123
+
124
+ expect(await getAdminAuthorizationUrl(manager)).toMatchObject({
125
+ error: { code: 'request.precondition_failed' }
126
+ });
127
+ });
128
+ });
129
+
130
+ describe('slates/auth.admin_callback.handle', () => {
131
+ it('runs the regular callback and discards its output by default', async () => {
132
+ let method = regularMethod();
133
+ let manager = await createManager({ ...method, adminSetup: {} });
134
+
135
+ expect(await handleAdminCallback(manager, { code: 'admin-code' })).toEqual({
136
+ jsonrpc: '2.0',
137
+ id: 'callback',
138
+ result: {}
139
+ });
140
+ expect(method.handleCallback).toHaveBeenCalledWith(
141
+ expect.objectContaining({
142
+ code: 'admin-code',
143
+ clientId: 'client-id',
144
+ callbackParams: {},
145
+ callbackState: {},
146
+ config: {}
147
+ })
148
+ );
149
+ });
150
+
151
+ it('requires a code when falling back to the regular callback', async () => {
152
+ let method = regularMethod();
153
+ let manager = await createManager({ ...method, adminSetup: {} });
154
+
155
+ expect(await handleAdminCallback(manager)).toMatchObject({
156
+ error: { code: 'request.bad', message: expect.stringContaining('authorization code') }
157
+ });
158
+ expect(method.handleCallback).not.toHaveBeenCalled();
159
+ });
160
+
161
+ it('uses the admin callback override without a code', async () => {
162
+ let method = regularMethod();
163
+ let adminHandleCallback = vi.fn(async () => ({ instructions: '1. Trust the app.' }));
164
+ let manager = await createManager({
165
+ ...method,
166
+ adminSetup: { handleCallback: adminHandleCallback }
167
+ });
168
+
169
+ expect(
170
+ await handleAdminCallback(manager, { callbackParams: { admin_consent: 'True' } })
171
+ ).toMatchObject({ result: { instructions: '1. Trust the app.' } });
172
+ expect(adminHandleCallback).toHaveBeenCalledWith(
173
+ expect.objectContaining({
174
+ code: undefined,
175
+ callbackParams: { admin_consent: 'True' },
176
+ callbackState: {}
177
+ })
178
+ );
179
+ expect(method.handleCallback).not.toHaveBeenCalled();
180
+ });
181
+
182
+ it('rejects methods without admin setup', async () => {
183
+ let manager = await createManager(regularMethod());
184
+
185
+ expect(await handleAdminCallback(manager, { code: 'admin-code' })).toMatchObject({
186
+ error: { code: 'request.precondition_failed' }
187
+ });
188
+ });
189
+ });
package/src/index.ts CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  import {
8
8
  createSlatesProviderProtoHandler,
9
9
  SLATES_PROTOCOL_VERSION,
10
+ type SlatesMessageAuthAdminCallbackHandleRequest,
11
+ type SlatesMessageAuthAuthorizationUrlGetRequest,
10
12
  type SlatesParticipant
11
13
  } from '@slates/proto';
12
14
  import {
@@ -344,6 +346,77 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
344
346
  return requestTraces.length > 0 ? { ...result, requestTraces } : result;
345
347
  };
346
348
 
349
+ let buildAuthorizationUrlContext = (
350
+ params: SlatesMessageAuthAuthorizationUrlGetRequest['params']
351
+ ) => ({
352
+ redirectUri: params.redirectUri,
353
+ state: params.state,
354
+ input: params.input,
355
+ clientId: params.clientId,
356
+ clientSecret: params.clientSecret,
357
+ scopes: params.scopes,
358
+ config: getAuthConfig()
359
+ });
360
+
361
+ let buildCallbackContext = (
362
+ params: Omit<SlatesMessageAuthAdminCallbackHandleRequest['params'], 'code'>
363
+ ) => ({
364
+ state: params.state,
365
+ redirectUri: params.redirectUri,
366
+ input: params.input,
367
+ clientId: params.clientId,
368
+ clientSecret: params.clientSecret,
369
+ scopes: params.scopes,
370
+ callbackParams: params.callbackParams || {},
371
+ callbackState: params.callbackState || {},
372
+ config: getAuthConfig()
373
+ });
374
+
375
+ let getTracedAuthorizationUrl = async (
376
+ params: SlatesMessageAuthAuthorizationUrlGetRequest['params'],
377
+ authenticationMethodName: string,
378
+ trace: {
379
+ functionName: string;
380
+ message: string;
381
+ successMessage: string;
382
+ metadata?: Record<string, unknown>;
383
+ },
384
+ getAuthorizationUrl: (
385
+ ctx: ReturnType<typeof buildAuthorizationUrlContext>
386
+ ) => Promise<{ url: string; input?: any; callbackState?: Record<string, any> }>
387
+ ) => {
388
+ let context = getEmptyContext();
389
+ let urlRes = await traceProviderCall(
390
+ {
391
+ component: 'auth',
392
+ functionName: trace.functionName,
393
+ message: trace.message,
394
+ successMessage: trace.successMessage,
395
+ metadata: {
396
+ authenticationMethodId: params.authenticationMethodId,
397
+ authenticationMethodName,
398
+ scopeCount: params.scopes.length,
399
+ inputKeyCount: getObjectKeyCount(params.input),
400
+ ...trace.metadata
401
+ },
402
+ onSuccess: result => ({
403
+ returnedInput: !!result.input,
404
+ hasCallbackState: !!result.callbackState
405
+ })
406
+ },
407
+ () =>
408
+ runWithContext(context, () =>
409
+ getAuthorizationUrl(buildAuthorizationUrlContext(params))
410
+ )
411
+ );
412
+
413
+ return withRequestTraces(context, {
414
+ authorizationUrl: urlRes.url,
415
+ input: urlRes.input,
416
+ callbackState: urlRes.callbackState
417
+ });
418
+ };
419
+
347
420
  manager.onNotification('slates/hello', async ({ params }) => {
348
421
  protocol.set(params.protocol);
349
422
  });
@@ -681,18 +754,7 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
681
754
  },
682
755
  () =>
683
756
  runWithContext(context, () =>
684
- authMethod.handleCallback({
685
- code: params.code,
686
- state: params.state,
687
- redirectUri: params.redirectUri,
688
- input: params.input,
689
- clientId: params.clientId,
690
- clientSecret: params.clientSecret,
691
- scopes: params.scopes,
692
- callbackParams: params.callbackParams || {},
693
- callbackState: params.callbackState || {},
694
- config: getAuthConfig()
695
- })
757
+ authMethod.handleCallback({ ...buildCallbackContext(params), code: params.code })
696
758
  )
697
759
  );
698
760
 
@@ -715,48 +777,108 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
715
777
  let authMethod = getAuthMethod(slate, params.authenticationMethodId);
716
778
 
717
779
  if ('getAuthorizationUrl' in authMethod) {
718
- let context = getEmptyContext();
719
- let urlRes = await traceProviderCall(
780
+ return getTracedAuthorizationUrl(
781
+ params,
782
+ authMethod.name,
720
783
  {
721
- component: 'auth',
722
784
  functionName: 'getAuthorizationUrl',
723
785
  message: 'Getting authentication authorization URL',
724
- successMessage: 'Authentication authorization URL retrieved',
786
+ successMessage: 'Authentication authorization URL retrieved'
787
+ },
788
+ ctx => authMethod.getAuthorizationUrl(ctx)
789
+ );
790
+ }
791
+
792
+ throw new ServiceError(
793
+ preconditionFailedError({
794
+ message: `Authentication method does not support authorization URL retrieval: ${params.authenticationMethodId}`
795
+ })
796
+ );
797
+ });
798
+
799
+ manager.onRequest('slates/auth.admin_authorization_url.get', async ({ params }) => {
800
+ getContextBasic();
801
+ let authMethod = getAuthMethod(slate, params.authenticationMethodId);
802
+
803
+ if (authMethod.type === 'auth.oauth' && authMethod.adminSetup) {
804
+ let { adminSetup } = authMethod;
805
+ return getTracedAuthorizationUrl(
806
+ params,
807
+ authMethod.name,
808
+ {
809
+ functionName: 'adminSetup.getAuthorizationUrl',
810
+ message: 'Getting admin setup authorization URL',
811
+ successMessage: 'Admin setup authorization URL retrieved',
812
+ metadata: { usesDefault: !adminSetup.getAuthorizationUrl }
813
+ },
814
+ ctx =>
815
+ adminSetup.getAuthorizationUrl
816
+ ? adminSetup.getAuthorizationUrl(ctx)
817
+ : authMethod.getAuthorizationUrl(ctx)
818
+ );
819
+ }
820
+
821
+ throw new ServiceError(
822
+ preconditionFailedError({
823
+ message: `Authentication method does not support admin setup: ${params.authenticationMethodId}`
824
+ })
825
+ );
826
+ });
827
+
828
+ manager.onRequest('slates/auth.admin_callback.handle', async ({ params }) => {
829
+ getContextBasic();
830
+ let authMethod = getAuthMethod(slate, params.authenticationMethodId);
831
+
832
+ if (authMethod.type === 'auth.oauth' && authMethod.adminSetup) {
833
+ let { adminSetup } = authMethod;
834
+ let ctx = { ...buildCallbackContext(params), code: params.code };
835
+
836
+ let handleCallback = async (): Promise<{ instructions?: string }> => {
837
+ if (adminSetup.handleCallback) return adminSetup.handleCallback(ctx);
838
+
839
+ let code = ctx.code;
840
+ if (!code) {
841
+ throw new ServiceError(
842
+ badRequestError({
843
+ message: `Admin setup callback did not include an authorization code: ${params.authenticationMethodId}`
844
+ })
845
+ );
846
+ }
847
+
848
+ await authMethod.handleCallback({ ...ctx, code });
849
+ return {};
850
+ };
851
+
852
+ let context = getEmptyContext();
853
+ let callbackRes = await traceProviderCall(
854
+ {
855
+ component: 'auth',
856
+ functionName: 'adminSetup.handleCallback',
857
+ message: 'Handling admin setup callback',
858
+ successMessage: 'Admin setup callback handled',
725
859
  metadata: {
726
860
  authenticationMethodId: params.authenticationMethodId,
727
861
  authenticationMethodName: authMethod.name,
728
862
  scopeCount: params.scopes.length,
729
- inputKeyCount: getObjectKeyCount(params.input)
863
+ hasCode: !!params.code,
864
+ hasCallbackState: !!params.callbackState,
865
+ usesDefault: !adminSetup.handleCallback
730
866
  },
731
867
  onSuccess: result => ({
732
- returnedInput: !!result.input,
733
- hasCallbackState: !!result.callbackState
868
+ returnedInstructions: !!result.instructions
734
869
  })
735
870
  },
736
- () =>
737
- runWithContext(context, () =>
738
- authMethod.getAuthorizationUrl({
739
- redirectUri: params.redirectUri,
740
- state: params.state,
741
- input: params.input,
742
- clientId: params.clientId,
743
- clientSecret: params.clientSecret,
744
- scopes: params.scopes,
745
- config: getAuthConfig()
746
- })
747
- )
871
+ () => runWithContext(context, handleCallback)
748
872
  );
749
873
 
750
874
  return withRequestTraces(context, {
751
- authorizationUrl: urlRes.url,
752
- input: urlRes.input,
753
- callbackState: urlRes.callbackState
875
+ instructions: callbackRes.instructions
754
876
  });
755
877
  }
756
878
 
757
879
  throw new ServiceError(
758
880
  preconditionFailedError({
759
- message: `Authentication method does not support authorization URL retrieval: ${params.authenticationMethodId}`
881
+ message: `Authentication method does not support admin setup: ${params.authenticationMethodId}`
760
882
  })
761
883
  );
762
884
  });
package/src/spec.test.ts CHANGED
@@ -222,4 +222,25 @@ describe('mapAuthMethod', () => {
222
222
  expect(mapAuthMethod(slate, method)).not.toHaveProperty('syncTokensAcrossConnections');
223
223
  }
224
224
  });
225
+
226
+ it('omits the adminSetup capability when undeclared so existing spec hashes stay stable', () => {
227
+ expect(
228
+ mapAuthMethod(slate, { ...oauthMethod, adminSetup: {} }).capabilities
229
+ ).toMatchObject({ adminSetup: { enabled: true } });
230
+ expect(mapAuthMethod(slate, oauthMethod).capabilities).not.toHaveProperty('adminSetup');
231
+ });
232
+
233
+ it('always advertises whether admin setup is required, defaulting to false', () => {
234
+ expect(
235
+ mapAuthMethod(slate, { ...oauthMethod, adminSetup: {} }).capabilities.adminSetup
236
+ ).toEqual({ enabled: true, required: false });
237
+ expect(
238
+ mapAuthMethod(slate, { ...oauthMethod, adminSetup: { required: false } }).capabilities
239
+ .adminSetup
240
+ ).toEqual({ enabled: true, required: false });
241
+ expect(
242
+ mapAuthMethod(slate, { ...oauthMethod, adminSetup: { required: true } }).capabilities
243
+ .adminSetup
244
+ ).toEqual({ enabled: true, required: true });
245
+ });
225
246
  });
package/src/spec.ts CHANGED
@@ -66,7 +66,10 @@ export let mapAuthMethod = <ConfigType extends {}, AuthType extends {}>(
66
66
  handleChangedInput: {
67
67
  enabled: !!m.onInputChanged
68
68
  },
69
- getProfile: { enabled: !!m.getProfile }
69
+ getProfile: { enabled: !!m.getProfile },
70
+ ...('adminSetup' in m && m.adminSetup
71
+ ? { adminSetup: { enabled: true, required: m.adminSetup.required === true } }
72
+ : {})
70
73
  },
71
74
 
72
75
  docs: m.docs ?? []